Skip to main content
Pre-release

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 daytona picks 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.v0 always uses its own production URL and ignores DAYTONA_API_URL. Migrating from the 0.x SDK, which documented DAYTONA_API_URL as its own API URL: unset it, or d.v2 sends requests to that (v0) host.
  • Credentials: d.v2 reads DAYTONA_* variables from the process environment only. d.v0 keeps the 0.x SDK's behaviour of also reading .env.local / .env, so a key kept only in a .env file reaches d.v0 but not d.v2; export it, or pass api_key= in DaytonaConfig.
  • DAYTONA_OTEL_ENABLED — enable client spans when daytona[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 explicit labels["name"] that disagrees with name= is rejected before any request);
  • derives its idempotency key from the name unless you pass idempotency_key: idempotency_key_for_name(name) is "name:" + name when 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 region is your job. (The exact region-hierarchy naming is still being settled; the scope is "one cell".)
  • Exact. create("py312", name="mate", cpu=2) followed by create("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

  • Daytona client (sync) and AsyncDaytona (async) — sandbox lifecycle through d.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), and d.v2.list(...) with any search filter (status, vm_class, region, labels, resource/timestamp bounds, sort, order, ...) routes to the same index. Timestamp filters are created_start_at / created_end_at, updated_start_at / updated_end_at, and last_activity_start_at / last_activity_end_at; starts are inclusive and ends are exclusive. Plain d.v2.list() stays on the read-your-writes GET /sandboxes path.
  • Sandbox (sync) / AsyncSandbox (async) — serializable handle with fork(), wait_for_state() and the process, fs, git, interpreter, pty, ports, lsp, docker and computer_use namespaces. 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_policy on create and create_macos, plus get_network_policy(), update_network_policy(network_policy=..., if_match=...), and reset_network_policy(if_match=...) over GET|PUT /sandboxes/{id}/network-policy. Responses expose desired vs applied state, the latest quoted ETag, pending or failed apply state, and the applied_start_id of the sandbox start that last applied the policy. Writes are full replacement only; reset is an explicit JSON null; the SDK does not auto-GET or auto-retry around stale If-Match. Domain allow-lists imply default-deny: omitting block_all while setting allow_domains lets the API normalize it to true, while an explicit block_all=False is rejected locally with NETWORK_POLICY_DOMAINS_REQUIRE_BLOCK_ALL. block_all=True with allow_cidrs and no allow_domains is the CIDR-only mode, which is not a DNS exfiltration boundary: DNS A lookups for any name are still forwarded upstream and only answers inside allow_cidrs come back, so a sandbox can carry data out in the names it looks up. When lookups must be contained, set allow_domains — allow_domains=[] resolves no tenant name (a domain list, even an empty one, also denies TCP 80/443 to allow_cidrs unless the name is listed); block_all=True with no allow list refuses every lookup except platform-internal names. The policy methods fail fast with UNSUPPORTED_OPERATION on 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-time network_policy on 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)

Source distribution for daytona 2.0.0a2
File Size Uploaded Provenance
daytona-2.0.0a2.tar.gz 827.0 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for daytona 2.0.0a2
File Interpreter ABI Platform
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Release history Release notifications | RSS feed

This release
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page