A real Python sandbox does not come from restricting the language from inside Python itself; it comes from putting an operating-system or runtime boundary around the interpreter, because in-process attempts to lock down Python have been broken again and again. If you need to run untrusted Python, code from users, from an LLM, from a plugin, the most important thing to internalize is that clever tricks with exec, eval, or a stripped-down set of builtins are not a security control. This guide explains why, then covers the isolation approaches that actually hold.
The trap: "restricted" execution inside Python
The intuitive approach is to run the untrusted code in the same process but take away the dangerous parts:
# This is NOT a security boundary. Do not rely on it.
safe_globals = {"__builtins__": {}}
exec(user_code, safe_globals)
This feels like a sandbox and is not one. Python is deeply introspective. Even with an emptied __builtins__, attacker code can walk the object graph, climbing from a harmless object through its class hierarchy to reach base classes, subclasses, and eventually references that lead back to os, subprocess, or file access. The history of "sandbox Python in Python" is a long list of people publishing a clever escape a week after someone published a clever cage. The old rexec and Bastion modules were removed from the standard library precisely because they gave a false sense of safety.
The takeaway: treat the interpreter itself as compromised the moment it runs untrusted code, and put your boundary outside it.
What a real Python sandbox looks like
Effective isolation means the untrusted interpreter runs somewhere it cannot reach anything valuable even if the code fully controls the Python process. There are a few practical tiers.
OS process and container isolation. Run the untrusted code in a separate process inside a locked-down container: no network, a read-only filesystem, dropped Linux capabilities, a non-root user, strict CPU and memory limits, and a seccomp profile that blocks dangerous syscalls. If the Python code escapes the interpreter, it still hits the container boundary. This is the workhorse approach for most teams.
docker run --rm \
--network none \
--read-only \
--user 65534:65534 \
--memory 256m --cpus 0.5 \
--security-opt no-new-privileges \
python:3.12-slim python /work/untrusted.py
Stronger kernel isolation. For higher-risk workloads, a sandboxed runtime like gVisor intercepts and filters syscalls before they reach the host kernel, and micro-VM approaches such as Firecracker give each execution a lightweight virtual machine. These raise the cost of a container escape substantially.
WebAssembly. Running Python compiled to WebAssembly (for example via Pyodide) executes the code inside a WASM runtime whose capability model denies filesystem and network access unless you explicitly grant it. This flips the default from "everything allowed unless removed" to "nothing allowed unless granted," which is the model you want for untrusted code.
Resource limits are part of the boundary
Isolation is not only about preventing access; it is about preventing abuse. Untrusted code that cannot read your files can still try to exhaust your resources: an infinite loop, a fork bomb, a multi-gigabyte allocation, or a regular expression crafted to run for hours. A Python sandbox that does not enforce CPU time, memory ceilings, process counts, and wall-clock timeouts is only half built. In the container approach above, --memory and --cpus do part of this; you also want an execution timeout that kills the whole container if it overruns.
Do not forget the dependency layer
A subtle failure mode: teams build a beautiful isolation boundary and then install arbitrary packages inside it. If the untrusted workload can pip install whatever it wants, or if your sandbox base image ships with a vulnerable library, you have reintroduced risk inside the box. Two habits help:
- Pin and vet the packages available in the sandbox rather than allowing open installation from the index.
- Scan the sandbox image itself. The base image and any preinstalled libraries have their own vulnerability profile, and a container escape becomes far more likely when the kernel or a system package inside has a known privilege-escalation flaw. Dependency and image analysis such as SCA applied to the sandbox image keeps the box you are trusting actually trustworthy.
LLM-generated code is the new common case
The reason "sandbox Python" gets searched so much lately is AI. Applications that let a language model write and run Python, code interpreters, autonomous agents, data-analysis assistants, are executing untrusted code by definition, because the model's output is not something you reviewed line by line. Everything above applies directly: never run model-generated Python in your application process, put it behind an OS or WASM boundary, deny network by default, and cap resources. The convenience of "just exec the model's code" is exactly the trap this guide warns against.
A practical decision guide
- Prototyping, low stakes, trusted-ish input: a hardened container with no network, read-only filesystem, non-root user, and resource limits covers most needs.
- Multi-tenant or genuinely hostile input: add gVisor or micro-VM isolation on top of the container.
- Browser or edge, capability-based safety wanted: WebAssembly via Pyodide gives deny-by-default isolation.
- Any of the above: enforce timeouts and memory caps, and scan the sandbox image for vulnerable components.
Whatever tier you pick, the constant is that the boundary lives outside the interpreter. The Safeguard Academy covers securing the container and dependency layers that these sandboxes are built on.
FAQ
Can I sandbox Python using only Python, without containers?
Not safely. In-process approaches that empty __builtins__ or restrict exec/eval are repeatedly bypassed because Python's introspection lets attacker code climb the object graph back to dangerous modules. Real isolation requires an OS-level or runtime boundary outside the interpreter.
What is the most practical way to run untrusted Python code?
For most teams, a locked-down container: separate process, no network, read-only filesystem, non-root user, dropped capabilities, a seccomp profile, and strict CPU/memory/time limits. For hostile or multi-tenant workloads, add gVisor or micro-VM isolation.
How do I sandbox Python code generated by an LLM?
Treat it as fully untrusted. Never run it in your application process. Execute it behind an OS or WebAssembly boundary with network disabled by default and hard resource and timeout limits, exactly as you would for any user-submitted code.
Do I need to secure the sandbox environment itself?
Yes. If untrusted code can install arbitrary packages, or the sandbox base image ships vulnerable libraries or an exploitable kernel, the isolation can be undermined. Pin and vet available packages and scan the sandbox image for known vulnerabilities.