56

I'm trying to read in a string representation of a Tuple from a file, and add the tuple to a list. Here's the relevant code.

raw_data = userfile.read().split('\n')
for a in raw_data : 
    print a
    btc_history.append(ast.literal_eval(a))

Here is the output:

(Decimal('11.66985'), Decimal('0E-8'))
Traceback (most recent call last):


File "./goxnotify.py", line 74, in <module>
    main()
  File "./goxnotify.py", line 68, in main
    local.load_user_file(username,btc_history)
  File "/home/unix-dude/Code/GoxNotify/local_functions.py", line 53, in load_user_file
    btc_history.append(ast.literal_eval(a))
  File "/usr/lib/python2.7/ast.py", line 80, in literal_eval
    return _convert(node_or_string)

  `File "/usr/lib/python2.7/ast.py", line 58, in _convert
   return tuple(map(_convert, node.elts))
  File "/usr/lib/python2.7/ast.py", line 79, in _convert
   raise ValueError('malformed string')
   ValueError: malformed string
Russia Must Remove Putin
  • 374,368
  • 89
  • 403
  • 331
Sinthet
  • 873
  • 2
  • 8
  • 13
  • 1
    If it's trusted input - could you eval it? – Jon Clements Jan 30 '13 at 18:58
  • Thats what I tried originally, it gave me SyntaxError: unexpected EOF while parsing. It is trusted input. – Sinthet Jan 30 '13 at 19:11
  • 3
    This *is* annoying... – Andy Hayden May 23 '13 at 02:04
  • Did you follow up why it is giving you that SyntaxError? Normally, eval() is something no one would ever recommend you use, but since it is trusted input, that would be the easiest way of doing what you need. – hfaran Aug 11 '13 at 22:46
  • See my answer to a similar question: https://stackoverflow.com/questions/15197673/using-pythons-eval-vs-ast-literal-eval/68732605#68732605 – Jay M Aug 10 '21 at 19:51

5 Answers5

49

ast.literal_eval (located in ast.py) parses the tree with ast.parse first, then it evaluates the code with quite an ugly recursive function, interpreting the parse tree elements and replacing them with their literal equivalents. Unfortunately the code is not at all expandable, so to add Decimal to the code you need to copy all the code and start over.

For a slightly easier approach, you can use ast.parse module to parse the expression, and then the ast.NodeVisitor or ast.NodeTransformer to ensure that there is no unwanted syntax or unwanted variable accesses. Then compile with compile and eval to get the result.

The code is a bit different from literal_eval in that this code actually uses eval, but in my opinion is simpler to understand and one does not need to dig too deep into AST trees. It specifically only allows some syntax, explicitly forbidding for example lambdas, attribute accesses (foo.__dict__ is very evil), or accesses to any names that are not deemed safe. It parses your expression fine, and as an extra I also added Num (float and integer), list and dictionary literals.

Also, works the same on 2.7 and 3.3

import ast
import decimal

source = "(Decimal('11.66985'), Decimal('1e-8'),"\
    "(1,), (1,2,3), 1.2, [1,2,3], {1:2})"

tree = ast.parse(source, mode='eval')

# using the NodeTransformer, you can also modify the nodes in the tree,
# however in this example NodeVisitor could do as we are raising exceptions
# only.
class Transformer(ast.NodeTransformer):
    ALLOWED_NAMES = set(['Decimal', 'None', 'False', 'True'])
    ALLOWED_NODE_TYPES = set([
        'Expression', # a top node for an expression
        'Tuple',      # makes a tuple
        'Call',       # a function call (hint, Decimal())
        'Name',       # an identifier...
        'Load',       # loads a value of a variable with given identifier
        'Str',        # a string literal

        'Num',        # allow numbers too
        'List',       # and list literals
        'Dict',       # and dicts...
    ])

    def visit_Name(self, node):
        if not node.id in self.ALLOWED_NAMES:
            raise RuntimeError("Name access to %s is not allowed" % node.id)

        # traverse to child nodes
        return self.generic_visit(node)

    def generic_visit(self, node):
        nodetype = type(node).__name__
        if nodetype not in self.ALLOWED_NODE_TYPES:
            raise RuntimeError("Invalid expression: %s not allowed" % nodetype)

        return ast.NodeTransformer.generic_visit(self, node)


transformer = Transformer()

# raises RuntimeError on invalid code
transformer.visit(tree)

# compile the ast into a code object
clause = compile(tree, '<AST>', 'eval')

# make the globals contain only the Decimal class,
# and eval the compiled object
result = eval(clause, dict(Decimal=decimal.Decimal))

print(result)
34

From the documentation for ast.literal_eval():

Safely evaluate an expression node or a string containing a Python expression. The string or node provided may only consist of the following Python literal structures: strings, numbers, tuples, lists, dicts, booleans, and None.

Decimal isn't on the list of things allowed by ast.literal_eval().

NPE
  • 486,780
  • 108
  • 951
  • 1,012
  • Bleh, I was hoping the fact its a tuple at its highest level would make it ok. Anyway, thanks. – Sinthet Jan 30 '13 at 18:54
  • I found a roundabout way to fix my problem I just laid a bounty for but I want to see a higher quality solution from someone else, particularly NPE since he already has some authority in this thread. – jdero Aug 05 '13 at 19:02
  • @jdero: If you are faced with untrusted input, the only secure solution is to create a custom parser to process the input string. – NPE Aug 06 '13 at 05:29
  • @NPE What would a custom parser look like? If you update your answer with a bit more beef I'd be glad to accept it. – jdero Aug 10 '13 at 00:47
  • Actually you would not need a custom *parser*, it is enough that you use the Python parser and then interpret AST trees – Antti Haapala -- Слава Україні Aug 11 '13 at 01:09
  • @AnttiHaapala If you would like to provide another, more thorough answer, I'd be glad to accept it. – jdero Aug 12 '13 at 00:37
  • That is helpful since I had an array and apparently I am not going to get very far with the array data structure – demongolem Aug 24 '18 at 17:23
7

in my case I solved with:

my_string = my_string.replace(':false', ':False').replace(':true', ':True')
ast.literal_eval(my_string)
Lukas
  • 313
  • 3
  • 5
  • I needed spaces after the colons: ````my_string = my_string.replace(': false', ': False').replace(': true', ': True')```` after moving from Python 3.6 to Python 3.10 (parsing a Django 4 queryset.explain(format='JSON')) – greg Jun 14 '23 at 13:38
4

Use eval() instead of ast.literal_eval() if the input is trusted (which it is in your case).

raw_data = userfile.read().split('\n')
for a in raw_data : 
    print a
    btc_history.append(eval(a))

This works for me in Python 3.6.0

HyperActive
  • 1,129
  • 12
  • 12
2

I know this is an old question, but I think found a very simple answer, in case anybody needs it.

If you put string quotes inside your string ("'hello'"), ast_literaleval() will understand it perfectly.

You can use a simple function:

    def doubleStringify(a):
        b = "\'" + a + "\'"
        return b

Or probably more suitable for this example:

    def perfectEval(anonstring):
        try:
            ev = ast.literal_eval(anonstring)
            return ev
        except ValueError:
            corrected = "\'" + anonstring + "\'"
            ev = ast.literal_eval(corrected)
            return ev
nojco
  • 39
  • 3