Execute Program

Python in Detail: Eval

Welcome to the Eval lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • We often refer to Python as "dynamic". For example, Python has dynamic types: we don't have to tell Python "this variable will hold an integer". A variable holding an integer can be reassigned to hold a string or a float.

  • Python is also dynamic in a second important way: the structure of the code itself can be changed at runtime. For example, we can add and remove methods and attributes on existing classes and objects. We can also evaluate code at runtime: while the program is running, we can store some Python code in a string, then execute the string.

  • The built-in eval function takes a string of Python code, runs it, and gives us its value.

  • >
    eval("1 + 3 + 5")
    Result:
    9Pass Icon
  • We can build up the string dynamically.

  • >
    x = 3
    y = 5
    code = f"1 + {x} + {y}"
    code
    Result:
    '1 + 3 + 5'Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    eval(code)
    Result:
    9Pass Icon
  • When we call eval, the expression can see the global scope and any local variables present at that point.

  • >
    x = 3
    eval("x + 1")
    Result:
    4Pass Icon
  • All of the usual built-ins are available: range, len, etc.

  • >
    eval("len([1, 2, 3])")
    Result:
    3Pass Icon
  • eval takes two optional arguments, globals and locals. By default, these are set to the usual globals and locals functions. We can control what variables the evaled code sees by passing a custom mapping (usually a dictionary) as the globals or locals.

  • >
    custom_globals = {
    "x": 2
    }
    x = 1
    eval("x+1", custom_globals)
    Result:
    3Pass Icon
  • eval only works with expressions, so it doesn't support statements like if, for, def, or even variable assignment via =. (In Python, variable assignments are statements, not expressions, so they have no value.) Trying to eval a statement is an error.

  • >
    x = 3
    eval("x = 4")
    x
    Result:
    SyntaxError: invalid syntax (<string>, line 1)Pass Icon
  • However, evaled code can still change data outside the eval statement. For example, it might call .append on a list.

  • >
    some_list = [1, 2, 3]
    eval("some_list.append(4)")
    some_list
    Result:
    [1, 2, 3, 4]Pass Icon
  • In an earlier lesson, we saw that built-ins like len are stored in the global __builtins__ dictionary.

  • >
    __builtins__["len"]([1, 2, 3, 4])
    Result:
    4Pass Icon
  • With eval, we can replace these built-ins by adding our own __builtins__ dictionary to the globals. When we do this, all of the normal built-ins, like len, disappear. Trying to access them is an error.

  • >
    isolated_env = {
    "x": 2,
    "__builtins__": {}
    }
    eval("len([x])", isolated_env)
    Result:
    NameError: name 'len' is not definedPass Icon
  • This lets us set up custom execution environments. For example, we might want to set up a bare learning context so students have to write code from scratch, without using any built-ins.

  • Above, we talked about using the globals and locals arguments to limit what the evaled code can access. That might sound like a security feature: maybe we can use it to safely execute code that comes from users.

  • Unfortunately, it doesn't work that way. Eval is never secure, no matter what we do!

  • Code running inside of eval has many attack vectors, even with the globals and locals removed. We'll focus on just one: using the __import__ built-in to import and use the os ("operating system") module. If the user can access os then they can run other programs, access files, make network connections, etc.

  • Below, we'll try to access the os.name variable, which contains a string. The content of the string isn't important; accessing it just tells us that we've definitely accessed the os module. Without any restrictions in place, we can simply use the __import__ built-in to import os.

  • >
    code = '__import__("os").name'
    eval(code)
    Result:
  • To prevent that, we might clear out the __builtins__ dictionary, which will stop the user from using __import__. To do that, we pass a globals dict with an empty __builtins__ dict. If the user tries to use __builtins__, they get an exception because the name doesn't even exist.

  • >
    code = '__import__("os").name'
    math = eval(code, {"__builtins__": {}})
    Result:
    NameError: name '__import__' is not definedPass Icon
  • At first, that seems to work. But in a language as dynamic as Python, there's always a clever workaround. The entire language is designed so that we can dynamically access and manipulate everything at runtime.

  • Here's what a workaround might look like. This code is very complicated, and we won't analyze how it does what it does. Just note that it manages to import os and give us the same string without using the __import__ built-in.

  • >
    code = "[class_ for class_ in ().__class__.__bases__[0] .__subclasses__() if class_.__name__ == 'BuiltinImporter'][0]() .load_module('os').name"
    eval(code, {"__builtins__": {}})
    Result:
  • What if we address this somehow? For example, that code uses the BuiltinImporter class. Maybe we can totally remove the BuiltinImporter class from the system?

  • If we did that, there would still be some other alternate attack vector. It's almost impossible to build a secure system by subtraction: we can't take a large, insecure system, then carefully subtract things away to get a secure system. If we plug this hole, attackers will find another hole.

  • Secure systems are usually secure by construction. To solve our eval problem, that would require a custom Python runtime specifically designed to "sandbox" the code, limiting what it can do.

  • Always remember: eval cannot be secured!

  • >
    "eval cannot be secured"
    Result:
    'eval cannot be secured'Pass Icon
  • Since eval is so risky, why does it exist at all? There are two main cases where we can safely use it:

    1. The code passed to eval is always known to be safe. No part of it ever comes from a user. Not even a single string or integer! It's very difficult to do this correctly.
    2. The code runs in a place where the user can't do any harm. Even if they took full control of the computer running the Python code, it wouldn't matter.
  • When we follow one of those rules, eval can be very useful. For example, Execute Program's source code uses eval!

  • Every code example that you've seen in this course ran via exec, which is like eval but can also run statements. When you type in your own code to complete a code problem, that runs via exec as well. We've been evaling code all along!

  • Knowing this, couldn't someone possibly write malicious code to access Execute Program's internal database? Fortunately, no.

  • One of Execute Program's deep design constraints is: the code runs in the user's own browser. By exposing eval within your own browser, we don't give you any new capabilities. This is a good example of Case 2 above: you already control your own web browser, so we can let you do whatever you want to it.