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
evalfunction takes a string of Python code, runs it, and gives us its value.>
eval("1 + 3 + 5")Result:
9
We can build up the string dynamically.
>
x = 3y = 5code = f"1 + {x} + {y}"codeResult:
'1 + 3 + 5'
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
eval(code)Result:
9
When we call
eval, the expression can see the global scope and any local variables present at that point.>
x = 3eval("x + 1")Result:
4
All of the usual built-ins are available:
range,len, etc.>
eval("len([1, 2, 3])")Result:
3
evaltakes two optional arguments,globalsandlocals. By default, these are set to the usualglobalsandlocalsfunctions. We can control what variables theevaled code sees by passing a custom mapping (usually a dictionary) as theglobalsorlocals.>
custom_globals = {"x": 2}x = 1eval("x+1", custom_globals)Result:
3
evalonly works with expressions, so it doesn't support statements likeif,for,def, or even variable assignment via=. (In Python, variable assignments are statements, not expressions, so they have no value.) Trying toevala statement is an error.>
x = 3eval("x = 4")xResult:
SyntaxError: invalid syntax (<string>, line 1)
However,
evaled code can still change data outside theevalstatement. For example, it might call.appendon a list.>
some_list = [1, 2, 3]eval("some_list.append(4)")some_listResult:
[1, 2, 3, 4]
In an earlier lesson, we saw that built-ins like
lenare stored in the global__builtins__dictionary.>
__builtins__["len"]([1, 2, 3, 4])Result:
4
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, likelen, disappear. Trying to access them is an error.>
isolated_env = {"x": 2,"__builtins__": {}}eval("len([x])", isolated_env)Result:
NameError: name 'len' is not defined
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
globalsandlocalsarguments to limit what theevaled 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
evalhas 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 theos("operating system") module. If the user can accessosthen they can run other programs, access files, make network connections, etc.Below, we'll try to access the
os.namevariable, which contains a string. The content of the string isn't important; accessing it just tells us that we've definitely accessed theosmodule. Without any restrictions in place, we can simply use the__import__built-in to importos.>
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 aglobalsdict 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 defined
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
osand 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
BuiltinImporterclass. Maybe we can totally remove theBuiltinImporterclass 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
evalproblem, that would require a custom Python runtime specifically designed to "sandbox" the code, limiting what it can do.Always remember:
evalcannot be secured!>
"eval cannot be secured"Result:
'eval cannot be secured'
Since
evalis so risky, why does it exist at all? There are two main cases where we can safely use it:- The code passed to
evalis 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. - 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.
- The code passed to
When we follow one of those rules,
evalcan be very useful. For example, Execute Program's source code useseval!Every code example that you've seen in this course ran via
exec, which is likeevalbut can also run statements. When you type in your own code to complete a code problem, that runs viaexecas well. We've beenevaling 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
evalwithin 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.