Eval and Exec Dangers — Core Concepts
What eval() and exec() Do
eval() takes a string, parses it as a Python expression, and returns the result. exec() takes a string, parses it as one or more Python statements, and executes them. Both accept optional dictionaries to control which names are available.
result = eval("2 + 3") # returns 5
exec("x = 10\nprint(x)") # prints 10
On the surface, they seem convenient — especially for dynamic behavior. But they are among the most dangerous functions in the standard library.
The Core Problem: Code Injection
When the string passed to eval() or exec() comes from an external source — user input, a web form, a database field, a configuration file, an API response — an attacker can inject arbitrary Python code.
# A "calculator" endpoint
user_input = request.form["expression"]
result = eval(user_input)
An attacker sends: __import__('os').system('rm -rf /')
The eval() call happily executes this, and now your server is running a destructive shell command. This is not theoretical — real applications have been compromised this way.
Why Restricting Globals Does Not Work
A common “fix” is to pass empty dictionaries for globals and locals:
eval(user_input, {"__builtins__": {}}, {})
This removes access to built-in functions like print and open. However, attackers have repeatedly found ways around this restriction. Python objects carry references to their types, which carry references to subclasses, which can reach back to dangerous functionality:
# This bypasses __builtins__ restrictions:
eval("().__class__.__bases__[0].__subclasses__()")
From the list of subclasses, an attacker can locate file-handling classes, subprocess classes, and import mechanisms. Every “sandbox” built on restricted eval() has eventually been broken.
Common Misuses and Their Alternatives
Dynamic attribute access
Bad: eval(f"obj.{attr_name}")
Good: getattr(obj, attr_name)
Dynamic function dispatch
Bad: eval(f"{func_name}(args)")
Good: Use a dictionary mapping names to functions:
dispatch = {"add": add_func, "delete": delete_func}
dispatch[func_name](args)
Parsing user expressions
Bad: eval(user_math_expression)
Good: Use ast.literal_eval() for safe evaluation of literal values, or use a proper expression parser like pyparsing or lark.
Configuration from files
Bad: exec(open("config.py").read())
Good: Use JSON, TOML, YAML, or configparser for configuration files. These formats cannot contain executable code.
Dynamic class creation
Bad: exec(f"class {name}: pass")
Good: type(name, (base,), attrs)
ast.literal_eval(): The Safe Subset
ast.literal_eval() evaluates a string containing only Python literals — strings, numbers, tuples, lists, dicts, sets, booleans, and None. It raises ValueError for anything else. This is safe for parsing data:
import ast
data = ast.literal_eval("{'name': 'Alice', 'age': 30}")
# Returns a regular dictionary — safe
It cannot execute function calls, imports, or attribute access, making it suitable for deserializing simple data from untrusted sources.
When eval/exec Are Acceptable
There are narrow cases where eval() or exec() are appropriate:
- Developer tools: REPLs, debuggers, and IDEs where the user IS the programmer
- Code generation frameworks: Where the generated code is fully controlled by the application (never user input)
- Testing: Dynamically generated test cases where the input is trusted
In all these cases, the string being evaluated comes from a trusted source — the developer themselves.
A Common Misconception
Some developers believe that eval() is safe as long as they “validate” the input with regex or string checks. This is false. Python’s syntax is complex enough that any validation short of full parsing will have gaps. If you find yourself writing regex to sanitize eval() input, you are building a sandbox with holes.
One thing to remember: eval() and exec() execute arbitrary code. No amount of input sanitization makes them safe for untrusted data. For every common use case, a safer alternative exists — ast.literal_eval(), getattr(), dictionary dispatch, or proper parsers. Use those instead.
See Also
- Ci Cd Why big apps can ship updates every day without turning your phone into a glitchy mess — CI/CD is the behind-the-scenes quality gate and delivery truck.
- Containerization Why does software that works on your computer break on everyone else's? Containers fix that — and they're why Netflix can deploy 100 updates a day without the site going down.
- Python 310 New Features Python 3.10 gave programmers a shape-sorting machine, friendlier error messages, and cleaner ways to say 'this or that' in type hints.
- Python 311 New Features Python 3.11 made everything faster, error messages smarter, and let you catch several mistakes at once instead of stopping at the first one.
- Python 312 New Features Python 3.12 made type hints shorter, f-strings more powerful, and started preparing Python's engine for a world without the GIL.