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.

pythonsecuritylanguage-implementation

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.