This release is a pre-release and may not be stable for production use.
Daytona Python SDK
The official Python SDK for Daytona sandboxes. Import module and package name
are both daytona.
Alpha. This SDK is published as alpha prereleases. Pin the exact version you want (see the release history on PyPI) — a bare
pip install --pre daytonapicks the highest prerelease on PyPI, which is not necessarily the newest alpha. The API may still change between alphas.
Install
pip install daytona==X.Y.ZaN # substitute the alpha you want
pip install 'daytona[otel]==X.Y.ZaN' # adds OpenTelemetry request spans
pip install 'daytona[v0-sdk]==X.Y.ZaN' # adds the v0 namespace (Daytona().v0)
Python 3.10+.
Quickstart
Daytona is the synchronous client. AsyncDaytona is the asynchronous one.
Every operation goes through a version namespace: d.v2 for the current API,
or d.v0 for the previous Daytona API (requires daytona[v0-sdk]; see
v0 API below).
Resource types (Sandbox, Image, ImageBuilder, …) live in daytona.v2 or
daytona.v0. Importing them from the package root raises an ImportError that
names where to import from. The clients are the other way round: the root is the
only way to build one (Daytona().v2, Daytona().v0), and neither namespace
exports Daytona, AsyncDaytona or DaytonaConfig (from daytona.v2 import Daytona raises an ImportError naming the root form). To annotate a stored
client, import its type from the root: DaytonaV2Client / AsyncDaytonaV2Client
(and DaytonaV0Client / AsyncDaytonaV0Client, which need the v0-sdk
extra).
from daytona import Daytona, DaytonaConfig
from daytona.v2 import ImageBuilder
with Daytona(DaytonaConfig(api_key="dtn_...")) as d:
builder = ImageBuilder.from_registry("python:3.12").pip_install("numpy")
image = d.v2.images.build(builder, name="py-numpy")
sandbox = d.v2.create(image.id, cpu=1.0, memory=1, wait_seconds=60)
result = sandbox.exec("python -V")
print(result.exit_code, result.output)
cell = sandbox.run_code("print(40 + 2)", timeout=30) # seconds
print(cell.logs.stdout)
d.v2.delete(sandbox)
with Daytona(...) as d: releases the client's connection pools when the block
ends (also on cancellation). A short script may instead let the interpreter
finish — teardown drops the sockets. In a long-lived process, prefer one
Daytona for the process's lifetime, or close each one you create: a client
you drop without closing keeps its idle keep-alive connections open until
interpreter exit.
d.v2 on the sync Daytona runs the async implementation on a private
background event loop and blocks the calling thread. Calling it from inside a
running event loop raises immediately instead of silently stalling that loop —
use AsyncDaytona there. d.v0 keeps the 0.x SDK's own behaviour: its sync
calls run (and block) wherever they are made, as they always did; from async
code use AsyncDaytona().v0.
A client created before fork() must not be used in the child; construct a new
one.
The root owns its namespace clients: close the root (with Daytona(...) or
root.close()), not root.v0 / root.v2 directly; a namespace client closed
on its own stays cached and fails on later calls. Closing is optional.
with Daytona(...) or close() releases the v2 client's
HTTP pools and stops its loop thread right away, and closes the v0 client's
event socket (and its OpenTelemetry provider, with otel_enabled); a client you never close is shut down the
same way at interpreter exit, so a short script can simply fall off the end.
Close explicitly in long-running processes that create many clients, or when you
need the connections gone now. Waits share one WebSocket per
cell among clients with matching connection settings and events configuration
on the same event loop; closing the last of them closes it.
AsyncDaytona
AsyncDaytona is the same SDK with coroutines instead of blocking calls. It
returns AsyncSandbox, AsyncProcessHandle, and so on — the async twin of
every type Daytona returns.
Fan out the way an orchestrator does — every handle is cheap and shares the client's connection pool:
import asyncio
from daytona import AsyncDaytona, DaytonaConfig
async def main() -> None:
async with AsyncDaytona(DaytonaConfig(api_key="dtn_...")) as d:
sandboxes = await asyncio.gather(
*(d.v2.create("worker-snapshot") for _ in range(100))
)
results = await asyncio.gather(
*(sb.exec("python train_step.py") for sb in sandboxes)
)
asyncio.run(main())
async with releases the client's connection pools when the block ends.
DaytonaConfig reads DAYTONA_API_KEY and DAYTONA_JWT_TOKEN from the
environment when the fields are omitted. Both credentials serve d.v0 and
d.v2. Additional env vars:
DAYTONA_ORGANIZATION_ID— scopes a JWT for the v0 namespace.DAYTONA_PROJECT_ID— scopes a JWT for the v2 namespace.DAYTONA_API_URL— overrides the v2 API endpoint; for test environments only. When unset, the SDK connects to the Daytona API.d.v0always uses its own production URL and ignoresDAYTONA_API_URL. Migrating from the 0.x SDK, which documentedDAYTONA_API_URLas its own API URL: unset it, ord.v2sends requests to that (v0) host.- Credentials:
d.v2readsDAYTONA_*variables from the process environment only.d.v0keeps the 0.x SDK's behaviour of also reading.env.local/.env, so a key kept only in a.envfile reachesd.v0but notd.v2; export it, or passapi_key=inDaytonaConfig. DAYTONA_OTEL_ENABLED— enable client spans whendaytona[otel]is installed.
Set request_timeout= (seconds) to bound individual HTTP calls,
connection_pool_maxsize= to cap both live and keep-alive HTTP connections,
and otel_enabled=True to emit client spans when daytona[otel] is installed.
The extra brings only opentelemetry-api; spans are exported only once your
application configures an OpenTelemetry SDK tracer provider (and exporter) —
the SDK never installs one itself.
Use exactly one authentication mode:
api_key="dtn_..."for a project API key.jwt_token="...", project_id="..."to exchange a signed-in user's JWT for a short-lived project-scoped platform token automatically.
The target field on DaytonaConfig is deprecated and applies only to
d.v0: pass target= to each v0 sandbox create
(CreateSandboxFromSnapshotParams(target="eu")) or region_id= to each v0
snapshot create instead. For v2, pass region= to each create.
v0 API
d.v0 gives access to the previous Daytona API through the 0.x Daytona SDK,
vendored into this package at a pinned release. It requires the v0-sdk extra —
accessing it without the extra raises ImportError naming the install command.
A program that only uses d.v2 never imports v0 code.
pip install 'daytona[v0-sdk]==X.Y.ZaN'
from daytona import Daytona, DaytonaConfig
from daytona.v0 import Sandbox # resource types live in the namespace
with Daytona(DaytonaConfig(api_key="dtn_...")) as d:
sandboxes = d.v0.list()
The root client is the only way to build a v0 client: daytona.v0 exports the
resource types and v0's own errors, but not Daytona, AsyncDaytona or
DaytonaConfig. from daytona.v0 import Daytona raises an ImportError that
names the root form.
Names that exist in both namespaces are also exported with a version suffix, so a single file can use both without renaming:
from daytona.v0 import Sandbox as SandboxV0 # or import SandboxV0 directly
from daytona.v2 import Sandbox as SandboxV2 # or import SandboxV2 directly
Migrating from v0:
| v0 code | SDK 2.x |
|---|---|
pip install daytona |
pip install "daytona[v0-sdk]" |
daytona.create(...) |
daytona.v0.create(...) |
from daytona import Sandbox |
from daytona.v0 import Sandbox |
from daytona import Daytona then Daytona() |
from daytona import Daytona then Daytona().v0 (not daytona.v0.Daytona, which SDK 2.x does not export) |
DaytonaConfig(api_url=...) / DAYTONA_API_URL for a custom API URL |
stay on SDK 0.x: SDK 2.x targets Daytona Cloud, and the root's .v0 ignores DAYTONA_API_URL |
Creating without an image
d.v2.create() with no image boots the platform default image, a Linux image
the operator publishes for every project. d.v2.images.list(platform="true")
lists the platform images; the default one has default=True. A platform image
can also be passed by name, for example d.v2.create("daytona-base"). When no
default is configured, the create raises DEFAULT_IMAGE_NOT_CONFIGURED (400).
Placement and safe create retries
d.v2.create() accepts an optional region= argument that places the sandbox
in a specific region. Omitting it (or passing "local") lets the server
resolve the project, organization, or platform default. Every d.v2.create*()
method accepts region=, including create_from_snapshot and create_from_build.
sandbox = d.v2.create("python:3.12", region="eu", wait_seconds=60)
Every create*() sends an idempotency key: the one you pass as
idempotency_key, else one derived from name (next section), else a random
one per invocation. If the create request times out or loses its connection,
SandboxCreateRequestTimeoutError / SandboxCreateConnectionError exposes
idempotency_key. Retry with that key and the identical source and create
arguments; do not start a fresh-key create to recover a lost response. A
changed request under the same key is a 409, never a silent replay of the
original parameters. Keys are scoped to the project and the same
coordinator/cell, not a fleet-global conflict check across direct cells;
changing regions requires a new operation and key. Deleting a sandbox releases
its key at delete intent, so the same key (or the same name) creates a fresh
sandbox afterwards; a retained volume reservation can still prevent reuse until
it is released.
Names, get-or-create and find_by_name()
Sandbox names are not unique — they are display metadata — but a named
create is also the handle of the one-sandbox-per-end-user pattern. Every
create*(name=…):
- stamps the name as a label,
labels["name"] == name, next to any labels you pass (an explicitlabels["name"]that disagrees withname=is rejected before any request); - derives its idempotency key from the name unless you pass
idempotency_key:idempotency_key_for_name(name)is"name:" + namewhen that is a key the API accepts verbatim (printable ASCII, no surrounding whitespace, ≤ 256 characters in total), else"name:sha256:" + hex(sha256(name)). The derivation is identical in the TypeScript SDK, so both SDKs are one client to the API's create ledger.
A repeated create("py312", name="mate") is therefore a get-or-create: the
same sandbox comes back (its access_token only on the first call), not a
second sandbox and not a 409 — v2 has no "409 on duplicate name". The exact
semantics, all server-side:
- Cell-scoped. The create ledger lives in the owning cell's Postgres, and
one cell can serve several co-located regions. An end user whose creates land
in two cells gets two sandboxes; pinning a given end user's creates to one
regionis your job. (The exact region-hierarchy naming is still being settled; the scope is "one cell".)
- Exact.
create("py312", name="mate", cpu=2)followed bycreate("py312", name="mate", cpu=8)is a 409, not a silent replay of the 2-CPU sandbox. Change the arguments and you need a new name or an explicit key. - Released on delete. Delete the sandbox and
create(..., name="mate")makes a new one.
find_by_name(name, region=…) returns the sandboxes carrying that exact name
label, newest first, as a list (names are not unique); deleted/deleting
ones are left out. It reads sandbox search with an exact name label match,
in the cell a create with the same region lands in (region defaults to
none, i.e., the server's default, exactly as on create*()). Search
indexes a new sandbox shortly after its create, so a lookup right after a
create can miss it — the get-or-create below stays safe, because the repeat
create derives the same key and returns the same sandbox. A deployment
without search raises SearchNotConfiguredError (501). Together:
existing = d.v2.find_by_name("mate")
sandbox = existing[0] if existing else d.v2.create("py312", name="mate")
# Two callers racing on an empty result still get ONE sandbox: both creates
# carry the key "name:mate", and the second is a replay of the first.
On AsyncDaytona the same pattern uses await:
existing = await d.v2.find_by_name("mate")
sandbox = existing[0] if existing else await d.v2.create("py312", name="mate")
What you get
Follow build logs
Image submission still returns immediately; its existing on_logs callback
reports the submitted job, not build output. Follow that build explicitly:
from daytona.v2 import BuildLogInterruptedError, ImageBuilder
image = d.v2.images.build(
ImageBuilder.from_dockerfile("FROM python:3.12\nRUN python -V\n"), name="my-worker"
)
try:
result = d.v2.images.follow_build_logs(
image.id,
on_logs=lambda chunk: print(chunk, end="", flush=True),
timeout=600, # seconds; zero/None adds no observation deadline
)
print(result.status, result.reason, result.output_complete)
except BuildLogInterruptedError as error:
print("Observation interrupted; last checkpoint:", error.cursor)
# For an existing sandbox's build (including an adopted build):
result = d.v2.get("sandbox-id").follow_build_logs(
on_logs=lambda chunk: print(chunk, end="", flush=True),
)
on_logs is required. Optional on_event receives a typed BuildLogEvent
union (event.type selects the variant), including states, gaps and source
unavailability. Without it, these diagnostics appear as [build logs: …]
messages in on_logs. Either callback may be a plain function or return an
awaitable, which is awaited before the next event (on AsyncDaytona). Build
failure is a result, not a connection error. output_complete describes the
requested cursor-to-end interval; gaps keep it false even after reconnection.
sandbox.build_logs() remains a one-shot string of currently retained output,
using the same discovered service as the follower (await sandbox.build_logs()
on AsyncDaytona). One-shot reads reject responses exceeding 2 MiB of
received body bytes with BUILD_LOG_PROTOCOL_ERROR, rather than silently
truncating them. This ceiling allows a 1 MiB retained log tail plus state and
diagnostic text.
A deadline in a state event is server execution metadata, not a client abort:
the follower still drains final output and waits for done with reason
deadline. Your timeout remains an independent observation limit.
request_timeout bounds discovery, one-shot reads, and error-response bodies,
including after headers arrive; it does not limit a healthy live log stream.
Each live stream uses a connection of its own, so a long observation never
holds up the client's other requests.
Pass cursor=... to resume a saved checkpoint. Automatic reconnects preserve
partial UTF-8 characters; a fresh follower starting mid-character can emit a
replacement character. Persist consumed output and its event cursor together
if you need application-level restart safety. Callback exceptions stop without
retry, since their side effects may already have happened. A timeout,
cancellation or keyboard interrupt closes the observation, not the build.
Source changes never restart from zero automatically. Unavailable output is
reported as an unavailable event; queued builds continue to be observed
until completion or a deadline.
Cancelling the task that awaits follow_build_logs propagates as ordinary
asyncio.CancelledError, so timeouts and task groups keep working. On
Daytona, sandbox.follow_build_logs(...) and
d.v2.images.follow_build_logs(...) block until the build finishes, and a
keyboard interrupt is surfaced as BuildLogInterruptedError with
code="BUILD_LOG_CANCELED" so the last checkpoint remains available.
Surface overview
Daytonaclient (sync) andAsyncDaytona(async) — sandbox lifecycle throughd.v2:create,get,list,iter,start,stop,pause,resume,delete. Both expose the same methods; the examples above use the sync form.- Sandbox search —
d.v2.search(...)over the eventually consistent, all-region search index (GET /sandboxes/search), andd.v2.list(...)with any search filter (status,vm_class,region,labels, resource/timestamp bounds,sort,order, ...) routes to the same index. Timestamp filters arecreated_start_at/created_end_at,updated_start_at/updated_end_at, andlast_activity_start_at/last_activity_end_at; starts are inclusive and ends are exclusive. Plaind.v2.list()stays on the read-your-writesGET /sandboxespath. Sandbox(sync) /AsyncSandbox(async) — serializable handle withfork(),wait_for_state()and theprocess,fs,git,interpreter,pty,ports,lsp,dockerandcomputer_usenamespaces. Sandbox handles borrow the client's shared transport; they do not need closing.- Network policy (Firecracker Linux sandboxes and macOS sandboxes; the API
rejects it for other Linux backends) — create-time
network_policyoncreateandcreate_macos, plusget_network_policy(),update_network_policy(network_policy=..., if_match=...), andreset_network_policy(if_match=...)overGET|PUT /sandboxes/{id}/network-policy. Responses expose desired vs applied state, the latest quoted ETag, pending or failed apply state, and theapplied_start_idof the sandbox start that last applied the policy. Writes are full replacement only; reset is an explicit JSONnull; the SDK does not auto-GET or auto-retry around staleIf-Match. Domain allow-lists imply default-deny: omittingblock_allwhile settingallow_domainslets the API normalize it totrue, while an explicitblock_all=Falseis rejected locally withNETWORK_POLICY_DOMAINS_REQUIRE_BLOCK_ALL.block_all=Truewithallow_cidrsand noallow_domainsis the CIDR-only mode, which is not a DNS exfiltration boundary: DNS A lookups for any name are still forwarded upstream and only answers insideallow_cidrscome back, so a sandbox can carry data out in the names it looks up. When lookups must be contained, setallow_domains—allow_domains=[]resolves no tenant name (a domain list, even an empty one, also denies TCP 80/443 toallow_cidrsunless the name is listed);block_all=Truewith no allow list refuses every lookup except platform-internal names. The policy methods fail fast withUNSUPPORTED_OPERATIONon Windows and GPU sandboxes once the handle knows its class (a handle that has not loaded it yet learns it on its first refresh; until then the API rejects the request); a create-timenetwork_policyon those routes is rejected like every other option the route does not accept (BAD_REQUEST). - Platform namespaces —
d.v2.images,d.v2.snapshots,d.v2.secret,d.v2.reservations,d.v2.mac_models.
Sandbox metadata and control-plane helpers
Every fetched sandbox now exposes the control-plane metadata already present on
the workload surface: labels, cpu / cpu_max, memory / memory_max,
disk / disk_max (GiB), vm_class, region,
persistent, spot, spot_evicted_at, the experimental
_experimental_gpu (may change or be removed without notice),
lifecycle_policy, forked_from, user, and error (network policy is read through get_network_policy()).
Use sandbox.set_labels(labels={...}) to replace the entire label set
({} clears it), and sandbox.entrypoint_logs() to read the sandbox's
entrypoint-session log as plain text. On AsyncDaytona prefix these with
await.
Use sandbox.update_env({...}, unset=(...)) to update the environment
inherited by processes, PTYs and sessions started afterwards. Running processes
keep their current environment, and unset removes only values previously set
through update_env(), not image-level environment entries.
Create sources
d.v2.create() is the fast path: pass a registered image name or an img_…
id and the SDK sends one POST /sandboxes request with your wait_seconds budget.
linux = d.v2.create("python:3.12", wait_seconds=60)
snapshot = d.v2.create_from_snapshot("worker-after-setup", wait_seconds=60)
d.v2.create_from_build() is the slow path: it builds an anonymous image
first, waits for that image to become ready, then creates the sandbox from the
image id. If the build fails, it raises ImageBuildFailedError and no sandbox
exists.
from daytona.v2 import ImageBuilder
builder = ImageBuilder.from_registry("python:3.12").run_commands("pip install numpy")
sandbox = d.v2.create_from_build(builder, cpu=2, memory=4, wait_seconds=60)
wait_seconds and create_timeout_seconds are budgets. The server holds create for up to its own
short cap, then the SDK long-polls the remainder and returns the sandbox in
whatever state it reached. wait_seconds=0 is the explicit async form.
macOS sandboxes
A macOS sandbox runs on a dedicated Mac reserved by your organization. Create
one with d.v2.create_macos(image, ...) (POST /sandboxes/macos);
create() makes Linux sandboxes.
image is a macOS image name
such as base-macos. cpu (whole vCPUs, at least 4), memory (GiB, at
least 4) and disk (GiB, at least 80: the whole guest disk, macOS included)
size the sandbox; each omitted value uses half of the Mac that claims the
sandbox, rounded down, on a split reservation, or all of it on a whole
reservation. The first claim fixes the size, which the sandbox reports from
then on (sandbox.memory and sandbox.disk in exact GiB, sandbox.reservation_id
for the reservation). name, idempotency_key, wait_seconds and
create_timeout_seconds work as on create(): a named create derives its
idempotency key from the name and
stamps labels["name"].
Creating a macOS sandbox, starting a stopped one, reserving a Mac or renewing a
reservation, and turning a reservation's auto-renew on require your organization's
macos_sandboxes feature flag: without it the API answers 403
FEATURE_NOT_ENABLED (raised as FeatureNotEnabledError). Reading sandboxes
and reservations, turning auto-renew off, pause() and resume() are not gated.
A sandbox without a size uses half of the Mac:
d.v2.create_macos("base-macos")
A reservation is one named Mac model; d.v2.mac_models.list() lists the models,
their CPU, memory and sandbox disk room, and the layouts they offer (split
runs up to two sandboxes on the Mac, whole runs one). Reserve a model, then
give the sandbox all of its CPU and memory and a 160 GiB disk:
reservation = d.v2.reservations.create(region="ash-1", model="m4pro-12c-24g")
d.v2.create_macos("base-macos", reservation=reservation.id, cpu=12, memory=24, disk=160, persistent=True)
Stop and pause need a persistent sandbox (persistent=True). Stop keeps the disk on the same Mac, and start and resume use that Mac. Delete erases the disk. At reservation end, a persistent sandbox's disk is saved as an anonymous image before the Mac is erased, and an ephemeral sandbox's disk is erased unsaved; a lost Mac saves persistent disks if it comes back within 7 days. If a Mac restarts before it is declared lost, a running persistent sandbox comes back stopped with its disk, a running ephemeral sandbox is lost, and stopped and paused sandboxes are unchanged. Create a sandbox from an image to continue on another Mac.
reservation picks the Mac reservation. Omitted, the sandbox goes on any reservation of your
organization in the region with room for its size, or, when none has room and
your organization's auto-reserve setting is on, the server buys a reservation of the
cheapest model that fits. reservation="new" always buys such a reservation for the
sandbox (auto_renew is accepted only here). A reservation ID targets that
reservation at claim. Acceptance does not reserve room; the sandbox can wait for room
or time out. Reservations are billed per 24-hour period; without model,
reservations.create() reserves the region's default model and never upgrades
an unavailable model to another one.
d.v2.mac_models.catalog(region=...) (GET /mac-catalog) shows, per model,
how many Macs a purchase can reserve now (available, advisory) and the
price of one reservation (None on a cell without billing).
reservations.create_bulk() reserves several Macs of one model in one
request: all of them, or none if fewer are free, unless min_count accepts
fewer. Every purchase and renewal carries an idempotency key; pass your own
so a retry after a lost response neither reserves more Macs nor pays for a
second period:
catalog = d.v2.mac_models.catalog(region="ash-1")
batch = d.v2.reservations.create_bulk(
region="ash-1", model="m4pro-12c-24g", count=3, idempotency_key="nightly-macs"
)
print(f"reserved {batch.reserved} of {batch.requested} Macs")
d.v2.reservations.renew(reservation.id, idempotency_key=f"renew-{reservation.id}-2")
The dashboard's desktop view opens the Mac's screen through your authenticated dashboard session. Click once in the desktop before typing, because macOS drops keys for the first few seconds of each connection. Customers cannot end a Mac reservation before its 24-hour period.
Without a reservation to use, a macOS create fails with ConflictError and
code == "NO_ACTIVE_RESERVATION" (or NO_FREE_SLOT when no reservation has room
for the size; MAC_DISK_FULL when the reservations with room have no free disk
space; AUTO_RESERVE_LIMIT_REACHED when auto-reserve holds its maximum number
of reservations; or NO_MAC_AVAILABLE when buying a reservation, with reservation="new"
or through auto-reserve, finds no fitting Mac in the region). MAC_DISK_FULL
never buys a reservation. Starting a stopped macOS sandbox uses its own disk on the
same Mac and reservation; it never buys a reservation. Reservations report model,
capacity, resources_used, origin (explicit or auto), auto_renew
and renewal_error; reservations.update(id, auto_renew=...) changes renewal
on a live reservation.
pause() and resume() work on persistent macOS sandboxes. pause() saves
the guest's memory on its Mac and waits for paused; resume() puts the
sandbox in assigned and waits for started. A paused sandbox keeps its Mac
slot, but pausing stops its reservation from auto-renewing: a reservation that
holds only paused sandboxes lapses at the end of its period. After a resume,
sandbox.mac_boot_mode is restored, or cold_after_restore_failure when the
memory could not be restored and the Mac booted the disk (error_reason says
why).
A macOS fork() copies the disk only: omit memory or pass memory=False
(memory=True answers NOT_SUPPORTED_FOR_CLASS). A fork of a running macOS
sandbox boots with a fresh machine identity; a fork of a paused or stopped
one rests stopped. The fork is made on the Mac that keeps the source when
that Mac has room for the fork's size. Otherwise, in a region with macOS
snapshots configured, the source's disk is copied through an anonymous image
to another Mac of the organization, and the fork waits for room there as a
create does. Without macOS snapshots in the region, or for a paused source on
a full Mac, the server answers NO_FREE_SLOT. fork(timeout=…) waits until
the macOS fork is started or stopped.
Processes
sandbox.exec() is the root alias of process.run(). process.run() executes
a command and awaits the result. process.start() returns a detached,
serializable ProcessHandle(sandbox_id, process_id) with logs(),
stream_logs(), wait(), stdin(), stdin_eof(), signal-aware kill(),
delete(), resize() and optional attach(). stdin() / stdin_eof() are for exec and
PTY handles only. timeout= is the caller wait budget (seconds), and
timeout=0 means wait unbounded. execution_timeout= is the process deadline
(seconds) for server-managed runs that support one. Rehydrated kind='code'
handles remain replay/control handles (logs() / wait() / kill()), not a
way to recover a parsed InterpreterResult on another worker. pty.list() is
the PTY-only view of process.list(kind="pty"), and pty.delete(id) kills
then cleans up that PTY record.
On AsyncDaytona, process.start() returns AsyncProcessHandle and methods
that return results require await.
Sessions
Create a Session for one persistent shell. Commands run one at a time, in
order; cd, exports and functions carry over. Set cwd and env at creation,
not on individual commands. The context manager deletes the session and its
commands on exit:
with sandbox.process.create_session(cwd="/app", env={"CI": "1"}) as session:
session.run("export BUILD_MODE=test")
result = session.run("npm test", execution_timeout=600)
command = session.start("npm run dev")
command.kill(signal="INT") # Stops this command; the session survives.
print(session.get().cwd) # The shell's current directory.
With AsyncDaytona, use await sandbox.process.create_session(), async with,
and await AsyncSession network methods. timeout bounds the caller's wait;
execution_timeout stops the command without ending the session (both seconds);
a command that has not stopped 5 seconds after its timeout ends the session
instead of blocking it.
Use session.to_json() and sandbox.process.connect_session(identity) to
reconnect without a request. process.list(kind="session") lists sessions;
process.list(session_id=session.id) lists their commands. Sessions survive
pause/resume but not a sandbox stop/start. On Windows, interrupting PowerShell
loops and cmdlets is not supported; external programs can be stopped. Session commands on Windows
cannot take stdin="open". In dash, BusyBox sh and mksh, an interrupt leaves only the
innermost shell function or sourced file; bash and zsh leave the whole command
(bash older than 4.4, macOS's /bin/bash, only when no DEBUG trap is set).
Files
read_text / write_text, read_bytes / write_bytes, upload_file() /
download_file(), bulk read_many / write_many, watch_dir() for directory
watches (get_new_events(), a single exit notification via on_exit= /
wait_for_exit()), and
presigned_download_url / presigned_upload_url for direct transfers. The
platform mints the link; expires_in_seconds (default 3600) must be between 1
and SIGNED_FILE_MAX_TTL_SECONDS (7 days) — links always expire.
sandbox.rotate_signing_key() retires every pre-signed URL minted so far
(they stop working within ~30 s); new URLs use the new key, which is never
returned to the client. List-like APIs return {items, next_cursor} objects;
iter() lazily drains every page.
upload_file() streams a local file in chunks, reports on_progress(bytes, total), accepts cancel= via any event-like object with is_set(), and can
set atomic=True to write through a temp file + rename. download_file()
streams to a unique temp file in the destination directory, renames it on
success, reports progress with Content-Length when the server sends it, and
removes that temp file on error or cancellation. write_bytes(),
write_text(), and write_many() also accept atomic=True. Only with
atomic=True, on a filesystem that cannot rename atomically, do
write_bytes() / write_text() / upload_file() raise
AtomicUploadUnsupportedError; write_many(atomic=True) keeps its
partial-success contract and reports those entries in the returned errors
(the failure message names the file and the atomic-rename limitation).
Pre-signed URLs are not available on Windows sandboxes
(UNSUPPORTED_OPERATION).
Interpreter
sandbox.run_code() is the root alias of interpreter.run(). Runs without an
explicit context share one per-language interpreter state inside the sandbox,
and that state's env is fixed when it is first created. Pass isolated=True
for a throwaway run or use create_context() / connect_context() /
list_contexts() for an independent named context. If an explicit named
context vanished, interpreter.run(..., context=...) / context.run() surface
the typed SessionNotFoundError; recreate the context yourself and decide what
to rerun. timeout= is enforced client-side (seconds): on expiry the kernel
is interrupted. Results are split into results[], logs{stdout,stderr},
error, and process. That rich result shape comes from the interpreter run
path itself; reconnecting a serialized code-process handle later does not
recreate it.
Git
clone(), pull() and push() use a dedicated 30-minute remote-operation
deadline by default, and each accepts a positive per-call timeout= override
(seconds).
clone() also accepts depth= for shallow clones and
insecure_skip_tls=True for one clone operation. pull() and push() accept
branch= and remote=; push() also accepts set_upstream=True.
commit(path, message, *, author=..., email=...) keeps the identity fields
keyword-only. Pass allow_empty=True to create an empty commit when Git would
otherwise refuse it.
Ports, LSP, computer use
ports owns both listener discovery and preview exposure:
list(), is_in_use(port), expose(port, public=...),
set_public(port, public=...), preview_link(port),
signed_preview_link(port, expires_in_seconds=...),
expire_signed_preview_link(port, token), paginated list_endpoints(...), and
iter_endpoints(...).
lsp.start(language_id, path_to_project) returns an LspServer handle with
did_open(uri), did_close(uri), completions(uri, position),
document_symbols(uri), workspace_symbols(query) and stop().
computer_use stays lifecycle-flat (status(), start(), stop()) and
splits actions into service, mouse, keyboard, screen, display,
a11y and recording.
Docker
Docker Engine starts automatically in sandboxes whose image ships it; there
is no create option. sandbox.docker.status() returns its state (ready,
starting, failed, not_present, disabled, unmanaged, deferred or
unsupported), who owns it, a reason, and the process_id whose logs hold
Docker's output. sandbox.docker.wait_until_ready(timeout=60) polls until
the state is ready and returns the status. It raises DockerNotReadyError
(error.status) as soon as Docker settles in a state that cannot become
ready (failed, not_present, disabled, unmanaged, unsupported, or
deferred with reason function-ready-template), and
DockerWaitTimeoutError when the budget (seconds) runs out while Docker
is still starting or deferred, or a status request is still unanswered at
the deadline. timeout=0 waits without a deadline. A Docker failure never
makes the sandbox itself unusable.
Set DAYTONA_DOCKER_START in the sandbox env to choose how Docker starts:
auto (default; start the image's dockerd when it ships one), entrypoint
(the image's own entrypoint starts Docker) or
off (Docker is not started; state disabled). A sandbox running an older
daemon that predates Docker status answers status() and
wait_until_ready() with NotFoundError (404).
Some environment variable names are reserved for the platform, among them
every DAYTONA_* name other than DAYTONA_SANDBOX_TOKEN,
DAYTONA_API_URL, DAYTONA_OTEL_ENDPOINT and DAYTONA_DOCKER_START. A
sandbox env that sets a reserved name fails the create with BadRequestError
(400) naming the key; the same rule applies to the keys an
update_env() call sets or unsets.
Waiting for lifecycle states
Use sandbox.wait_for_state(states, terminal_states=('destroying', 'destroyed', 'error'), *, timeout=None)
to wait for a public sandbox state. timeout is the caller wait budget in
seconds; None and 0 wait unbounded. If the sandbox reaches a terminal state
before any target state, the SDK raises a typed conflict error with the
observed public state attached as error.state.
sandbox.wait(until='started', timeout=60) is the one-state convenience form:
it returns the sandbox on success, raises RequestTimeoutError when the budget
runs out, and raises SandboxTerminalStateError when a terminal state wins
first. timeout=None and timeout=0 wait unbounded.
Errors
The package root exports the error hierarchy both namespaces share:
DaytonaError and one class per category — DaytonaBadRequestError
(alias DaytonaValidationError), DaytonaAuthenticationError,
DaytonaForbiddenError (alias DaytonaAuthorizationError),
DaytonaNotFoundError, DaytonaTimeoutError, DaytonaConflictError,
DaytonaGoneError, DaytonaUnprocessableEntityError, DaytonaRateLimitError,
DaytonaInternalServerError, DaytonaServiceUnavailableError and
DaytonaConnectionError. They catch API errors from d.v0 and d.v2 alike,
category for category. One exception: d.v0's transport failures (a refused
connection, a timeout) arrive as v0's plain DaytonaError, as in the 0.x SDK,
so catch them with DaytonaError, not DaytonaConnectionError /
DaytonaTimeoutError:
import daytona
try:
d.v2.get("missing")
except daytona.DaytonaNotFoundError as e: # also catches a v0 not-found error
print(e.status_code, e.code)
The v2 classes (from daytona.v2 import NotFoundError, …) expose status_code,
code, source, and trace_id, and have code-specific subclasses
(ProcessNotFoundError, SandboxTerminalStateError, …) so you can catch as
narrowly as you need. Version-specific errors are imported from daytona.v2
(or daytona.v0).
A request refused because your organization has hit one of its limits (for
example the maximum number of concurrent sandboxes) raises
LimitExceededError — a RateLimitError that also carries the limit's name
and counts as limit, current and max. Other rate-limited requests keep
raising plain RateLimitError.
Versioning and releases
See clients/VERSIONING.md and clients/CHANGELOG.md in the
repository.
Metadata
Release files for daytona 2.0.0a2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| daytona-2.0.0a2.tar.gz | 827.0 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| daytona-2.0.0a2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 2.4 MB
Release files / daytona-2.0.0a2.tar.gz
| Download URL | daytona-2.0.0a2.tar.gz |
|---|---|
| Size | 827.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
afcba1057bc0939cd87d24580e3a3bc629c49f8318f59240c73ae88072d2b4f1
|
|
BLAKE2b-256 checksum How to use checksums |
ba4ec00ff4c2f98b9ff183afd42453ddc82fd6dce33ef3f26d2b8ad2aa3bed84
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.
Transparency logRelease files / daytona-2.0.0a2-py3-none-any.whl
| Download URL | daytona-2.0.0a2-py3-none-any.whl |
|---|---|
| Size | 1.6 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
53be285df307d26e6cbcf59884281ff07a9b1b1e20f1d6cbb73336875a1a09de
|
|
BLAKE2b-256 checksum How to use checksums |
a1b49cbe46ccf31f338d0bcab00386b798cf0f1c51e421c04b978d260473ac6b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.
Transparency logSecurity for daytona 2.0.0a2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for daytona-2.0.0a2.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.