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](installsmodal). - Parameters:
app_name,image(a prebuiltmodal.Image),pip_packages,python_executable.
How To Obtain Modal Credentials​
- Create a free account at modal.com.
- Authenticate the CLI (opens a browser, writes
~/.modal.toml):This is enough for local use: the Modal SDK reads credentials frommodal token new~/.modal.tomlautomatically. - Alternatively, create a token in the Modal dashboard (Settings → API Tokens)
and export
MODAL_TOKEN_IDandMODAL_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.