Run untrusted Python in a separate execution environment with only the files and permissions the task needs. Apply network restrictions and execution limits, collect a bounded result, and clean up afterward. Vercel Sandbox provides isolated microVMs that your Python application can create and control through its SDK.
This pattern applies to model-generated scripts, uploaded exercises, and customer-defined transformations. The application remains responsible for deciding who may run code and what happens to the result. In a Python application, set up controller authentication and deployment before exposing code execution to users.
Copy link to headingWhy aren't a virtual environment and exec() enough?
Python virtual environments separate a project's installed packages from other Python installations. They don't create a security boundary around files or network access, so code runs with the process's operating-system permissions.
Similarly, exec() executes code inside the Python process that calls it. Changing the globals dictionary or removing selected built-ins doesn't turn arbitrary Python into a safe language subset.
Starting a child process separates execution from your application's interpreter, but a process on the same machine can still inherit access to local files and credentials. Put the submitted program behind an enforced execution boundary before relying on command-line flags or code validation as additional controls.
Copy link to headingWhat stays in the application, and what enters the sandbox?
Keep the execution controller in your application. It authenticates the caller, chooses the sandbox configuration, and retrieves output. The controller writes source to a remote file, then runs it with the sandbox's Python interpreter.
Avoid copying your application environment into the sandbox. An agent-generated script that summarizes a CSV needs neither your backend's token for creating sandboxes nor unrelated uploads from the same account.
Check upload ownership before staging a file. You can combine Sandbox execution with Blob storage to keep uploads and results outside the sandbox.
Copy link to headingRun a bounded Python job
The controller requires Python 3.10 or newer. Complete the Sandbox authentication setup first so the local controller can load its credentials from .env.local. The example uploads the submitted source without passing local environment variables to the remote command.
Copy link to heading1. Install the controller dependencies
In a Python project environment, install the packages used by the example:
python -m pip install "vercel==0.12.2" "vercel-sandbox==0.11.0" python-dotenvInstall these packages in the controller. The submitted program uses Python's standard library and requires no additional packages or outbound network access.
Copy link to heading2. Save the execution controller
Save this as run_python.py. Its output writer keeps up to 20,000 characters from stdout and stderr combined, then consumes and discards further text.
import asyncioimport ioimport jsonimport sysfrom datetime import timedelta
from dotenv import load_dotenvfrom vercel import sandboxfrom vercel.sandbox import NetworkPolicy, SandboxResources
class LimitedOutput(io.TextIOBase): def __init__(self, limit: int = 20_000): self.remaining = limit self.parts: list[str] = [] self.truncated = False
def writable(self) -> bool: return True
def write(self, text: str) -> int: kept = text[:self.remaining] if kept: self.parts.append(kept) self.remaining -= len(kept) if len(kept) < len(text): self.truncated = True return len(text)
def getvalue(self) -> str: return "".join(self.parts)
async def run_untrusted(source: str) -> dict: if len(source.encode("utf-8")) > 100_000: raise ValueError("Source exceeds 100,000 bytes")
output = LimitedOutput() async with sandbox.create_sandbox( persistent=False, network_policy=NetworkPolicy.deny_all(), execution_time_limit=timedelta(seconds=60), resources=SandboxResources(vcpus=2, memory=4096), ) as box: await box.fs.write_text("submitted.py", source) result = await box.run_command( "python", ["submitted.py"], kill_after=30, stdout=output, stderr=output, )
return { "exit_code": result.returncode, "output": output.getvalue(), "output_truncated": output.truncated, }
async def main() -> None: load_dotenv(".env.local") if len(sys.argv) != 2: raise SystemExit("Usage: python run_python.py submitted.py") with open(sys.argv[1], "rb") as source_file: source_bytes = source_file.read(100_001) if len(source_bytes) > 100_000: raise ValueError("Source exceeds 100,000 bytes") result = await run_untrusted(source_bytes.decode("utf-8")) print(json.dumps(result))
if __name__ == "__main__": asyncio.run(main())The controller uses the SDK's command and lifecycle APIs. kill_after=30 sets a server-side SIGKILL deadline of 30 seconds. The 60-second session limit also covers file staging and other processes.
The example sets persistent=False to discard the filesystem when the session stops. The context manager attempts to stop and destroy the sandbox on exit, even if the block raises an exception. Cleanup failures raise SandboxCleanupError.
The writer caps retained text. It doesn't limit remote output, incoming traffic, or SDK memory used to receive each output event. Production runners should also stop excessive-output jobs and enforce service-wide resource limits.
Copy link to heading3. Submit a program and inspect the result
Save a separate file named submitted.py:
values = [3, 5, 7]print(sum(values))Run the controller:
python run_python.py submitted.pyFor this program, the expected result is:
{"exit_code": 0, "output": "15\n", "output_truncated": false}The local controller reads the file without importing or executing it. The remote interpreter receives a fixed filename as a separate command argument, so submitted source never becomes part of a shell command in the controller.
Because run_command() defaults to check=False, nonzero exit codes remain available in the result. SDK or infrastructure failures can raise exceptions; handle those separately from a program that ran and failed. If execution ends without a result, report that condition without inventing an exit status.
Copy link to headingHow should you install Python packages?
Prepare dependencies before executing the submitted program. For a known workload, install approved package versions in a trusted environment, then preserve it as a Sandbox snapshot. Start each job from that prepared filesystem to avoid repeating installation or carrying one submission's changes into the next.
Package installation can execute third-party code, so keep it inside an isolated preparation environment. Approve package versions before installation, since a malicious dependency can affect the environment before the user's script starts.
If setup requires package downloads, finish installation and restrict network access before uploading or running untrusted source. Follow the Python guide's prepared test-runner example for this sequence.
For a program that must call an external service, permit only the access needed for that job. An allowed hostname can still expose powerful API operations. Use scoped credentials or a tool in your application that authorizes each request.
Copy link to headingHow do you handle results and generated files?
An exit code of zero says that the process reported success. It doesn't establish that the output answers the question correctly. Submitted code can print any value, change its own tests, or terminate before performing the requested work.
If the program returns structured data, validate it against an application-defined schema. For a calculation, check task-specific invariants, such as whether record counts match the input and whether totals reconcile. Keep the input revision and submitted source with the job record when reproducibility matters.
Treat generated artifacts as untrusted files. Retrieve only expected paths and check size and format before storing or serving them. Escape logs when displaying them in a browser. An HTML report needs a different serving policy from a plain-text result because it can contain active content.
For charts or transformed datasets, see how to combine Sandbox with MCP for data analysis. Retrieve the files you need before destroying the sandbox.
Copy link to headingWhat needs to change before accepting public submissions?
Before exposing this runner through a public API, add authentication and enforce request-size limits before reading submissions into memory. Limit concurrent jobs per user so one caller cannot create an unbounded number of sandboxes.
Define what happens when the client disconnects or the controller crashes. The remote execution deadline remains useful even when the application can no longer issue cleanup requests. Track sandbox and job identifiers so you can reconcile jobs whose completion status is uncertain.
Retries need the same care. Computation-only scripts can often run again, but scripts that send a webhook or update a record may have completed those actions before the response was lost. Use operation identifiers and application-side checks to prevent duplicate effects.
When a longer process must pause for a decision, a durable workflow can coordinate dynamic JavaScript with human approval.
Copy link to headingFrequently asked questions
Copy link to headingCan I use a Python virtual environment as a security sandbox?
No. Virtual environments separate Python packages, while programs retain the permissions of the operating-system process that runs them. Untrusted Python needs a separate execution boundary with explicit file and network access controls.
Copy link to headingCan I make exec() safe by removing built-ins?
Removing built-ins does not provide dependable isolation for arbitrary Python. The submitted program still runs within the calling interpreter, and restricting available names is not a substitute for an enforced execution boundary. Keep untrusted execution outside your application process.
Copy link to headingCan untrusted Python install packages inside a sandbox?
Installation is possible when the environment and network policy permit it, but dependencies introduce executable code of their own. Prefer a prepared environment with approved versions when the workload is known. If setup needs network access, finish it before restricting access and running the submission.
Copy link to headingDoes a successful exit code prove the submitted code passed its task?
It proves only that the process reported a successful exit. The application needs independent checks for correctness, and a grading system must account for submissions that can modify tests or forge output. Keep evaluation authority outside the submitted program's control.
Copy link to headingWhat happens when the program runs too long?
Set a server-side command deadline and a separate lifetime for the execution environment. The runner in this article uses a 30-second command deadline that sends SIGKILL and a 60-second session limit. If execution ends without a result, report that condition separately from a program that completed with an exit code.