Skip to main content

Modal

Runs code in a Modal cloud sandbox, providing fully isolated, on-demand containers with configurable images and secrets.

State persists between calls, though Modal itself offers nothing that holds a Python namespace: what it offers is a container and exec, a process at a time. So the variant holds one itself — a single python -u -c session process is started with the sandbox and fed one snippet at a time. A session process that cannot be started, or that goes away mid-session, drops the sandbox back to a process per snippet — working, merely stateless — rather than failing the run. Configure the image with additional pip packages as needed.

  • Requirements: code-sandboxes[modal] (installs modal).
  • Parameters: app_name, image (a prebuilt modal.Image), pip_packages, python_executable.

How To Obtain Modal Credentials​

  1. Create a free account at modal.com.
  2. Authenticate the CLI (opens a browser, writes ~/.modal.toml):
    modal token new
    This is enough for local use: the Modal SDK reads credentials from ~/.modal.toml automatically.
  3. Alternatively, create a token in the Modal dashboard (Settings → API Tokens) and export MODAL_TOKEN_ID and MODAL_TOKEN_SECRET.

Do you need both MODAL_TOKEN_ID and MODAL_TOKEN_SECRET?

  • For environment-based auth (CI/CD, containers, hosted runners): yes, you need both values because Modal authenticates with a token pair (public id + secret).
  • For local development with modal token new: not necessarily. The SDK can authenticate directly from ~/.modal.toml.

If you need environment variables, you can export them from your local config:

python - <<'PY'
import pathlib
import tomllib

cfg = tomllib.loads(pathlib.Path("~/.modal.toml").expanduser().read_text())
profile = cfg.get("default", cfg)
token_id = profile.get("token_id")
token_secret = profile.get("token_secret")
if token_id and token_secret:
print(f"export MODAL_TOKEN_ID={token_id}")
print(f"export MODAL_TOKEN_SECRET={token_secret}")
else:
raise SystemExit("Could not find token_id/token_secret in ~/.modal.toml")
PY

Usage​

from code_sandboxes import Sandbox

with Sandbox.create(
variant="modal",
pip_packages=["numpy"],
) as sandbox:
result = sandbox.run_code("import numpy as np; print(np.arange(3).sum())")
print(result.stdout) # "3"

Environments​

An Environment built for Modal is an image id, im-… — pulled from_aws_ecr off the approved Datalayer base, with your lock installed by uv pip sync --require-hashes on top. A published name is worth having for operability, but a launch always pins the id: a name moves when the same build is published again, and the id is the only reference that names one build's image. Modal answers an identical image definition with the image it already has, so two builds of the same lock can share one id.

Bring your own account: a build runs in the environment owner's own Modal workspace, from the same MODAL_TOKEN_ID/MODAL_TOKEN_SECRET pair Sandbox.create itself accepts (above) — never this package's ambient token.

Modal ignores the image's own USER, so every build step runs as root regardless of what the base sets — but a launched sandbox's identity is put back where it actually matters: the session driver Sandbox.create starts inside the container drops itself to the contract's 1000:100 before it starts executing anything, for an Environment-built image specifically (never for a plain ModalSandbox with no image id — general Modal sandbox usage here is unaffected). Confirmed live: a launched sandbox's identity check now passes, where it used to report uid: 0.

The sandbox is held by sleep infinity, not the base's Jupyter. Modal keeps the base image's CMD, start-jupyter.sh, under the builder's ENTRYPOINT, where Docker would reset it, so a sandbox started with no command ran a Jupyter server as its main process. That server exits within a minute and the sandbox ends with it: "Modal Sandbox is shutting down", a few commands in. Since 1.9.36 the artifact's CMD is sleep infinity, and ModalSandbox names sleep infinity when it launches an artifact, which covers images built by earlier releases too. Jupyter is started by exec when it is asked for (prepare_jupyter_server).

GPU versions. On Modal a GPU is a launch option, not part of the image: a GPU version's image builds like any other on the CUDA base, and its sandboxes start on the GPU the version names. The build's smoke test does that (gpu="L4", or "H100:2" for two) and adds the contract's check 11: the GPUs asked for are visible, and the image's CUDA is the version's. A GPU Modal does not offer — it offers T4, L4, A10G, L40S, A100, A100-40GB, A100-80GB, H100, H200, B200 — is refused at validate, before anything is built.