Skip to main content
Guides

How to Sandbox Python: Run Untrusted Code Safely (Tested)

By Mithun··8 min read
Quick Answer

You can't sandbox Python with eval/exec tricks — stripping __builtins__ still lets attacker code reach every class in the process and run os.system in one line (proven below, tested). Real options, weakest to strongest: RestrictedPython (blocks dangerous syntax, but not a true sandbox); OS-level isolation — Docker, gVisor, or Firecracker micro-VMs plus seccomp (strong, but server infrastructure); and WebAssembly — Python compiled to WASM runs with no filesystem, no sockets, and no OS access by default. For a zero-setup WASM sandbox you can use right now, our online Python compiler runs code entirely in your browser.

Running untrusted Python is a real need — a coding playground that runs visitor submissions, a backend executing AI-generated code, or an analyst opening a suspicious script. The approach almost everyone reaches for first is a "restricted" eval/exec, and it is dangerously broken. Let me show you exactly how broken, then the approaches that actually work.

Everything below was run on Python 3.14 — including a real, one-line escape out of a naive sandbox.

The trap: eval/exec "sandboxes" escape in one line

The classic idea: run the user's code with the builtins removed, so they can't call open, __import__, or eval. It looks airtight — and it's defeated by a single expression, because Python's introspection lets you climb from *any* object back to every class loaded in the process:

# a naive "sandbox": strip every builtin, then run the user's code
SANDBOX = {"__builtins__": {}}
# the attacker submits this — it uses NO builtins, only introspection:
escape = (
"[c for c in ().__class__.__base__.__subclasses__() "
"if c.__name__=='BuiltinImporter'][0]().load_module('os')"
".system('echo ESCAPED-THE-SANDBOX')"
)
eval(escape, SANDBOX)
the naive sandbox and the escape — tested on 3.14
ESCAPED-THE-SANDBOX # os.system executed — sandbox defeated
# it's not subtle: from an "empty" sandbox you still reach every
# class loaded in the interpreter —
>>> len(eval("().__class__.__base__.__subclasses__()", {"__builtins__": {}}))
177
real output — the OS command ran

().__class__ is tuple, .__base__ is object, and object.__subclasses__() lists every class in the process — including one that can import os. From there it's os.system, and the attacker owns your machine. No builtins required.

Never run untrusted code with a restricted `eval`/`exec`. There is no blocklist that closes every introspection path — Python is too dynamic. If you can run arbitrary Python in your process, you have no sandbox at all.

RestrictedPython — better, but not a real sandbox

The most mature language-level option is RestrictedPython (from the Zope project). Instead of a blocklist, it compiles a *restricted subset* of Python and refuses dangerous constructs — including the dunder-attribute access the escape above depends on:

from RestrictedPython import compile_restricted, safe_globals
src = "().__class__.__base__.__subclasses__()" # the escape from above
bytecode = compile_restricted(src, "<user>", "eval")
eval(bytecode, dict(safe_globals))
RestrictedPython blocks the escape — tested
SyntaxError: Line 1: "__subclasses__" is an invalid attribute name
because it starts with "_".
# RestrictedPython refuses dunder access at COMPILE time — escape blocked.
real output

RestrictedPython's own maintainers are explicit: it "is not a sandbox system or a secured environment." It's excellent for a *trusted* subset — spreadsheet-style formulas, template logic — but Python's dynamism means language-level restriction alone shouldn't be your only barrier against a hostile attacker.

OS-level isolation: containers and micro-VMs

If a server has to execute genuinely untrusted code, the real boundary is the operating system, not the language. In rough order of strength:

  • Containers (Docker/Podman) — Linux namespaces (PID, network, mount) isolate the process. Good, but a container shares the host kernel, so a kernel exploit escapes it. Drop capabilities, use a read-only rootfs, and disable networking.
  • `seccomp` — a kernel feature that restricts which system calls a process may make (in the strictest mode, essentially only read, write, exit, sigreturn). Layer it on top of a container to shrink the attack surface.
  • gVisor — a user-space kernel that intercepts syscalls, adding a strong isolation layer between the code and the real kernel.
  • Micro-VMs (Firecracker) — a real VM boundary with near-native speed and millisecond boot. This is what serverless platforms use to run arbitrary customer code safely.

This tier is the right answer for a backend that runs user code at scale — but it's infrastructure you build, operate, and pay for. For most people who just need to *run some Python safely*, it's far more than they want to manage.

WebAssembly: the zero-setup sandbox

The modern sweet spot is WebAssembly. Python compiled to WASM (via Pyodide) runs inside a sandbox that was *designed* for untrusted code from day one and hardened in browsers for a decade. By default a WASM module has no filesystem, no network sockets, no environment variables, and no OS access — it can only touch what the host explicitly hands it.

That flips the escape from the first section on its head: even if attacker code reaches os inside a WASM Python, os.system has nothing to call — there's no shell, no real filesystem, and socket can't open a connection, because the WASM runtime simply doesn't expose them. The isolation is enforced *below* Python, so Python's dynamism can't defeat it.

The trade-off is compatibility: pure-Python packages install fine, but libraries with C extensions (NumPy, pandas) need a Pyodide-compiled build — the runtime ships many, but not every package on PyPI works.

Try it: a Python sandbox in your browser

Our free online Python compiler is a WASM sandbox — it runs your code through Pyodide entirely in your browser tab. That gives you the isolation properties above with zero setup:

  • Nothing runs on a server — the code executes in *your* browser, so there's no backend to attack and nothing is uploaded.
  • No sockets, no real filesystem, no OS — a reverse shell or credential-stealer simply can't function; the APIs it needs aren't there.
  • It even installs packagesimport auto-installs pure-Python wheels from PyPI via micropip, still inside the sandbox.

One honest caveat that applies to *every* online code tool: the sandbox protects your machine, but don't paste real secrets (API keys, passwords) into any web editor — a hostile snippet could exfiltrate data you put in the sandbox via a network fetch. It can't reach anything on your computer, but it can read what you gave it.

Which sandbox should you use?

ApproachIsolationUse it for
Restricted eval/execNone (escapable)Never — it is not a sandbox
RestrictedPythonLanguage-levelTrusted subsets: formulas, template logic
Container + seccomp / gVisorStrong (OS)A backend running user code at scale
Micro-VM (Firecracker)Strongest (VM)Multi-tenant untrusted code, serverless
WebAssembly (Pyodide)Strong, zero-privilegeBrowser/edge, zero-setup, portable

Short version: never trust a restricted eval. For a trusted subset, RestrictedPython. For a server running hostile code, containers or micro-VMs. And for running untrusted Python right now with nothing to install, a WASM sandbox like our in-browser compiler is the fastest safe path.

Run Python in a sandbox — free

Our online compiler is a WASM sandbox: it runs your code in your browser, with no server, no sockets, and no filesystem. Nothing uploaded.

Open the Python Sandbox

Free tools mentioned here

Related guides

Frequently asked questions

How do I run untrusted Python code safely?

Don't use a restricted eval/exec — it's escapable in one line. Use real isolation instead: RestrictedPython for a trusted language subset, a container (Docker) with seccomp or gVisor for a server, a micro-VM (Firecracker) for multi-tenant workloads, or WebAssembly to run Python in a sandbox with no filesystem, network, or OS access. For zero setup, a WASM-based online compiler runs untrusted code in your browser.

Is eval() with restricted builtins a safe Python sandbox?

No. Stripping __builtins__ does not stop introspection: expressions like ().__class__.__base__.__subclasses__() still reach every class in the process, including ones that import os and run shell commands. A restricted eval is not a security boundary — it's defeated by a single line, as demonstrated on Python 3.14.

What is RestrictedPython and is it a sandbox?

RestrictedPython (from the Zope project) compiles a restricted subset of Python and blocks dangerous constructs like dunder-attribute access at compile time. Its maintainers state it is not itself a sandbox or secured environment — it's meant for running trusted, limited code (like template or formula logic), and shouldn't be your only defense against a hostile attacker.

Can WebAssembly sandbox Python?

Yes — this is one of the best modern options. Python compiled to WASM (via Pyodide) runs in a sandbox with no filesystem, network sockets, or OS access unless the host explicitly grants them, and the isolation is enforced below Python so its dynamic features can't escape it. The main limitation is that C-extension packages need a Pyodide-compiled build.

Is an online Python compiler safe to run untrusted code?

It depends on where the code runs. A server-side runner executes code on someone's machine and needs heavy isolation. A WASM-based one (like ours) runs entirely in your own browser: nothing is uploaded, no server executes the code, and the sandbox has no sockets or real filesystem, so classic malware can't function. Still, never paste real secrets into any web tool.

Keep reading