Build a Stateful Daytona Python Sandbox

Build a Python lab that runs isolated, stateful code in Daytona sandboxes.

Introduction

30 Second Summary

When an assistant writes code for you, that code still needs somewhere safe to run. Running every experiment on your own computer can put your files within reach.

In this project, you will build a terminal-based Python lab that runs code inside a Daytona hosted sandbox. The lab's output reveals where state persists and where context isolation blocks access.

What You'll Build

You will watch one terminal run move from a missing variable to preserved state to an isolated error before ending with Sandbox deleted.

By the end of this project, you'll have:

  • A remote execution lab that sends Python snippets to Daytona and prints their results in macOS Terminal.
  • A stateful execution experiment that keeps score available across calls inside one interpreter context.
  • An isolation and cleanup proof that surfaces an expected NameError before the lab deletes its contexts and sandbox.
  • Secret Mission: Turn the lab into a small evaluator that gives every code job a fresh context.

Are there any prerequisites?

Bring basic comfort with Python and terminal commands. This project uses Visual Studio Code on macOS.

You do not need a Daytona account before starting because Step 1 covers its setup.

Before We Start

Before any setup begins, commit to proving how a Daytona sandbox handles code across separate runs and isolated interpreter contexts. You will also make callback-based error reporting and defensive cleanup visible.

Set Up Daytona and the Local Project

The sandbox lab needs authenticated access to Daytona before it can control a hosted sandbox. Your free credit balance must also be positive before the project creates any billable resource.

In this step, you will create scoped access for the lab. You will also prepare a virtual environment in Visual Studio Code with Daytona Python SDK 0.207.0.

In this step, get ready to:
  • Confirm that your Daytona Wallet has a positive Free credit balance.
  • Create a scoped Daytona API key for sandbox operations.
  • Prepare the local Python environment with the pinned Daytona SDK.
Create and check your Daytona account

The Daytona Wallet shows whether free credit is available for the lab. Checking this balance creates no sandbox.

  • Visit app.daytona.io in your browser.
  • Create a Daytona account from the sign-up page if you do not have one.
  • Sign in to your Daytona account.
  • Open the Daytona Wallet.
  • Inspect the Free credit balance.

✔️ My Free credit balance is positive

Your positive balance confirms that the project can continue within its free-credits-only budget.

ⓧ My Free credit balance is zero

A zero balance falls outside this project's free-credits-only budget. No billable resource has been created.

  • Stop here before creating a Daytona sandbox.

An API key lets the local SDK authenticate with Daytona. Its permissions limit the sandbox operations that your program can request.

Keep Your API Key Private

Creating a credential can feel sensitive. This key receives only the permissions required by the lab.

Copy the key when Daytona shows it. Keep the value out of screenshots and source files.

  • Return to the Daytona Dashboard.
  • Open the API key area.
  • Click Create Key.
  • Enter daytona-sandbox-lab as the key name.
  • Select write:sandboxes in the permissions list.
  • Select delete:sandboxes in the permissions list.
  • Click Create.
  • Copy the API key when Daytona shows it.

That sensitive setup is complete. Your scoped key can create and delete the short-lived sandboxes used by this lab.

Prepare the Python environment in Visual Studio Code

The local project needs its own Python environment so the Daytona package stays separate from your other projects. The version check protects the lab from installing the SDK with an unsupported interpreter.

Why Use the Python SDK?

The Daytona Python SDK lets your local program create sandboxes through Python. This makes the application-to-sandbox control pattern visible.

A dashboard focuses on manual lifecycle operations. The SDK gives the lab the programmatic orchestration pattern used by coding agents.

  • Press Cmd+Space on macOS or the Windows key on Windows to open your search bar.
  • Type Visual Studio Code into the search bar.
  • Press Enter to open Visual Studio Code.

Visual Studio Code opens with its welcome screen or your most recent workspace.

  • Click File in the menu bar.
  • Click Open Folder....
  • Select your Desktop in the file dialog.
  • Click New Folder.
  • Enter daytona-sandbox-lab as the folder name.
  • Click Open.

You will see daytona-sandbox-lab at the top of the Explorer sidebar. This confirms that Visual Studio Code has the correct folder open.

  • Click View in the Visual Studio Code menu bar.
  • Click Terminal to open the integrated terminal.
  • Create the virtual environment and activate it by running these commands:
python3 -m venv tutorial-env
source tutorial-env/bin/activate
python --version

What Do These Commands Do?

  • The first line creates an isolated Python environment in the tutorial-env folder.
  • The second line directs the terminal to use that environment.
  • The final line reports the Python interpreter used inside the environment.

✔️ I see Python 3.10 or higher

Your interpreter is supported when the version is 3.10 or newer. It must also be earlier than 4.

ⓧ I see an older version

The current interpreter is below the Daytona SDK requirement. Install a supported Python release before recreating the environment.

  • Visit the official Python downloads for macOS.
  • Download a macOS installer for Python 3.10 or newer.
  • Choose a release earlier than Python 4.
  • Complete the macOS installer.
  • Remove the existing tutorial-env folder from the Visual Studio Code Explorer sidebar.
  • Restart Visual Studio Code with the daytona-sandbox-lab folder open.
  • Open a fresh integrated terminal from the View menu.
  • Recreate the environment with the supported interpreter by running:
python3 -m venv tutorial-env
source tutorial-env/bin/activate
python --version

What Does the Retry Confirm?

The recreated environment uses the Python installation available to the fresh terminal. Continue when the reported version is at least 3.10 and earlier than 4.

Still Seeing the Older Version?

The previous Python installation may still be your terminal's default. Follow the path guidance from the macOS installer before repeating the version check.

Ask for help with selecting the supported Python interpreter.

ⓧ Command not found

The terminal cannot find Python. Install a supported release before creating the environment.

  • Visit the official Python downloads for macOS.
  • Download a macOS installer for Python 3.10 or newer.
  • Choose a release earlier than Python 4.
  • Complete the macOS installer.
  • Restart Visual Studio Code with the daytona-sandbox-lab folder open.
  • Open a fresh integrated terminal from the View menu.
  • Create the environment with the installed interpreter by running:
python3 -m venv tutorial-env
source tutorial-env/bin/activate
python --version

What Does the New Check Confirm?

The version output proves that the fresh terminal can find Python. Continue when it reports a supported version.

Python Still Unavailable?

Restart your Mac so the terminal can load the newly installed Python command. Reopen the project folder before trying the environment commands again.

Ask for help with finding Python after the macOS installation.

The environment now has a supported interpreter. The project can record the exact Daytona dependency it needs.

  • Click the New File icon in the Visual Studio Code Explorer sidebar.
  • Enter requirements.txt as the file name.
  • Add the pinned Daytona dependency by copying this code into requirements.txt:
daytona==0.207.0

What Does This Dependency Pin Do?

The package name daytona installs the Daytona Python SDK. The exact version 0.207.0 keeps the later API calls aligned with this project.

  • Save requirements.txt.
  • Confirm that requirements.txt remains visible in the Explorer sidebar.

Dependency File Missing?

Check that daytona-sandbox-lab appears at the top of the Explorer sidebar. Create requirements.txt inside that folder if it was saved elsewhere.

Ask for help with finding the dependency file.

✔️ Awesome, I've got everything!

Your dependency file is saved with the required Daytona SDK pin.

ⓧ I'd like to double check the full code

daytona==0.207.0

What Should Match?

Your complete requirements.txt file contains this single dependency line. Match the package name and version exactly.

The first installation often takes one or two minutes while pip downloads the SDK dependencies. A short pause during this command is expected.

  • Install the dependency from requirements.txt by running:
python -m pip install -r requirements.txt

What Does This Installation Do?

The active Python interpreter asks pip to read requirements.txt. The pinned SDK is installed inside tutorial-env.

Your system-wide Python packages remain unchanged. The lab now has the SDK it needs.

You will see package download and installation progress in the terminal. The command returns to the prompt when installation finishes.

Dependency Installation Failed?

Confirm that the terminal is still using tutorial-env. Check that requirements.txt contains the exact dependency shown above.

Ask for help with installing the Daytona SDK in the virtual environment.

Load the API key without placing it in source code

An environment variable makes the API key available to the SDK through the active terminal. Your Python source code stays free of the credential.

Protect the Credential

The next command contains your live API key after you replace the placeholder. Keep that terminal line out of screenshots.

This command makes the credential available only through the current terminal environment. It does not add the key to requirements.txt.

  • Replace YOUR_API_KEY in the command below with the key you copied from Daytona.
  • Export the API key into the active integrated terminal by running:
export DAYTONA_API_KEY=YOUR_API_KEY

What Does This Command Do?

The command adds DAYTONA_API_KEY to the current terminal environment. Processes started from this terminal can use it to authenticate with Daytona.

A fresh terminal requires another export. This keeps the credential out of the project files.

Before you run the final check, which Daytona package version do you expect the activated environment to report?

  • List the packages installed in tutorial-env by running:
python -m pip freeze

What Does This Verification Prove?

The active Python interpreter asks pip to print installed distributions in requirements-compatible form. The output verifies the package version inside tutorial-env.

You should see daytona==0.207.0 in the output. That confirms the pinned Daytona SDK is ready.

Daytona Missing From the Package List?

Reactivate tutorial-env if your terminal is using another Python environment. Repeat the dependency installation before checking the package list again.

Ask for help with finding the missing Daytona package.

  • Return to the Daytona Wallet in your browser.
  • Confirm that the Free credit balance remains positive.
  • Switch back to the Visual Studio Code integrated terminal.
  • Scroll until the API key export command is out of view.

Your Daytona access is ready. Your isolated Python environment now has the exact SDK version required by the lab.

Your account and local environment are ready. Next, you will run two independent snippets and watch the second one lose the first snippet's state.

Expose Stateless State Loss

Your authenticated Daytona setup is ready. The next question is whether two remote Python runs share what they learn.

Stateless execution treats each code request as an independent interpreter session. This step turns that boundary into visible terminal output before you add persistence later.

In this step, get ready to:
  • Create a short-lived Daytona sandbox with guaranteed cleanup.
  • Run two Python snippets through separate stateless executions.
  • Confirm the sandbox deletes after both executions finish.
Create a safe sandbox lifecycle

A hosted sandbox gives your local script an isolated place to execute remote code. A finally block keeps deletion on the cleanup path if code inside try reports a problem.

Keep the Cleanup Path

Creating this sandbox starts consuming your available free credit. The lab deletes it during every normal cleanup path.

A started sandbox bills reserved vCPU. It also bills RAM. It also bills disk.

A deleted sandbox stops billing those resources. Keep the finally block in place whenever you experiment.

  • Use the file sidebar in Visual Studio Code to create sandbox_lab.py inside the open daytona-sandbox-lab folder.
  • Paste the following code into sandbox_lab.py:
from daytona import Daytona, DaytonaError


def main() -> None:
    daytona = Daytona()
    sandbox = None

    try:
        sandbox = daytona.create()
        print(f"Sandbox created: {sandbox.id}")

    finally:
        if sandbox is not None:
            try:
                sandbox.delete(wait=True)
                print("Sandbox deleted.")
            except DaytonaError as cleanup_error:
                print(f"Sandbox cleanup failed: {cleanup_error}")


if __name__ == "__main__":
    main()

What does this code do?

  • Daytona() initializes the client with the API key exported in your active terminal.
  • sandbox = None gives the cleanup path a safe value before sandbox creation begins.
  • daytona.create() creates the remote sandbox.
  • sandbox.delete(wait=True) deletes the sandbox before the program reports that cleanup finished.
  • DaytonaError lets the script report a cleanup failure without hiding it.
  • Save sandbox_lab.py.
  • Test the creation and cleanup lifecycle by running this command in the active terminal:
python sandbox_lab.py

What should I see?

You should see a sandbox identifier after Sandbox created:. The final line should be Sandbox deleted..

Good work. Your script can now create a remote sandbox and clean it up in one run.

Sandbox Not Reaching Cleanup?

Confirm tutorial-env is still active in the terminal.

Confirm DAYTONA_API_KEY remains exported in that same terminal session.

If the problem continues, help me troubleshoot why my Daytona sandbox is not being created or deleted.

Add the two-run experiment

The next experiment asks the same sandbox to run two snippets. The first defines score. The second asks for that value without defining it again.

  • In sandbox_lab.py, find print(f"Sandbox created: {sandbox.id}").
  • Insert the following block directly below that line:
        print("\n1. Stateless execution")
        first_run = sandbox.process.code_run(
            "score = 41\nprint(f'First run score: {score}')",
            timeout=60,
        )
        print(first_run.result)

        second_run = sandbox.process.code_run(
            "print(f'Second run score: {score}')",
            timeout=60,
        )
        print(f"Exit code: {second_run.exit_code}")
        print(second_run.result)

How does the experiment work?

  • first_run defines score as 41 inside its snippet.
  • second_run references score inside a separate call.
  • exit_code reveals whether the second snippet completed successfully.
  • result carries the output from each remote execution back to your terminal.
  • timeout=60 gives each remote execution up to 60 seconds to finish.
  • Save sandbox_lab.py.
Predict what survives

The script now tests the handoff between independent runs. Before you run it, do you expect the second snippet to remember score?

  • Execute the completed experiment from the active terminal by running this command:
python sandbox_lab.py

What should I see?

  • The first execution prints First run score: 41.
  • The second execution reports a nonzero exit code.
  • The second result includes NameError because its interpreter does not contain score.
  • The final line prints Sandbox deleted. after the expected failed snippet.

You have made the state boundary visible. The final cleanup line confirms the short-lived sandbox is gone.

Output or Cleanup Looks Different?

If the second run prints a score, confirm that score = 41 appears only inside the first snippet.

If the deletion message is missing, compare the indentation under finally with the full file below.

For another pair of eyes, help me compare my stateless Daytona experiment with the expected state-loss flow.

✔️ Awesome, I've got everything!

Your requirements.txt remains unchanged. Your saved sandbox_lab.py now reproduces stateless state loss and guarantees a sandbox deletion attempt.

ⓧ I'd like to double check the full code

Your existing requirements.txt should still contain daytona==0.207.0.

  • Compare sandbox_lab.py against this full reference:
from daytona import Daytona, DaytonaError


def main() -> None:
    daytona = Daytona()
    sandbox = None

    try:
        sandbox = daytona.create()
        print(f"Sandbox created: {sandbox.id}")

        print("\n1. Stateless execution")
        first_run = sandbox.process.code_run(
            "score = 41\nprint(f'First run score: {score}')",
            timeout=60,
        )
        print(first_run.result)

        second_run = sandbox.process.code_run(
            "print(f'Second run score: {score}')",
            timeout=60,
        )
        print(f"Exit code: {second_run.exit_code}")
        print(second_run.result)

    finally:
        if sandbox is not None:
            try:
                sandbox.delete(wait=True)
                print("Sandbox deleted.")
            except DaytonaError as cleanup_error:
                print(f"Sandbox cleanup failed: {cleanup_error}")


if __name__ == "__main__":
    main()

Your stateless baseline is complete. Next, you will give both snippets one persistent interpreter context so score can survive the handoff.

Preserve State in a Context

Your stateless Daytona test proved that a second execution starts without score. An AI agent that works across several turns needs its working values to survive between calls.

This step adds a persistent interpreter context. The same context carries score from one snippet into the next.

In this step, get ready to:
  • Capture remote output through callbacks.
  • Preserve score across two calls in one context.
  • Delete the tracked context before deleting the sandbox.
Add output callbacks

A stateful run delivers output through callbacks. Separate handlers receive standard output, standard error, or interpreter errors.

  • Find the current import line at the top of sandbox_lab.py:
from daytona import Daytona, DaytonaError

Why Does the Import Need to Grow?

Daytona creates the client. DaytonaError lets the cleanup path report SDK failures.

The stateful handlers also need typed objects for output messages and interpreter errors.

  • Replace the current import area with this callback section:
from daytona import Daytona, DaytonaError, ExecutionError, OutputMessage


def handle_stdout(message: OutputMessage) -> None:
    print(f"[STDOUT] {message.output}", end="")


def handle_stderr(message: OutputMessage) -> None:
    print(f"[STDERR] {message.output}", end="")


def handle_error(error: ExecutionError) -> None:
    print(f"[ERROR] {error.name}: {error.value}")

What Do These Callbacks Do?

  • The handle_stdout() callback prints normal output from remote code.
  • The handle_stderr() callback labels standard error output.
  • The handle_error() callback prints the interpreter error name with its value.
  • Save sandbox_lab.py.
  • Confirm the callback definitions load by running this command:
python sandbox_lab.py

What Should You See?

You should see the stateless experiment run as before. The final line should be Sandbox deleted..

This proves Python can load the new callback definitions without disrupting the existing sandbox lifecycle.

Does the Script Stop Before Creating a Sandbox?

Confirm that every imported name matches the updated import line. Check that the three callback functions sit above main().

Ask for help with the callback definitions in sandbox_lab.py.

Create the persistent context

Each interpreter context has its own global namespace. Reusing one context lets the second snippet access values created by the first.

  • Find the local setup at the start of main():
    daytona = Daytona()
    sandbox = None

Why Track Another Resource?

The sandbox variable tracks the hosted sandbox. The program also needs a collection for every interpreter context it creates.

That collection gives the cleanup path a reliable list of resources to delete.

  • Replace that local setup with the tracked version below:
    daytona = Daytona()
    sandbox = None
    contexts = []

What Changes Here?

The contexts list starts empty. Every user-created context enters this list as soon as the program creates it.

  • Find print(second_run.result) inside the try block.
  • Insert the first stateful experiment directly below it by pasting this code:
        print("\n2. Persistent interpreter context")
        primary_context = sandbox.code_interpreter.create_context()
        contexts.append(primary_context)

        sandbox.code_interpreter.run_code(
            "score = 41\nprint(f'Primary score: {score}')",
            context=primary_context,
            on_stdout=handle_stdout,
            on_stderr=handle_stderr,
            on_error=handle_error,
            timeout=60,
        )

How Does the First Stateful Run Work?

  • The section label separates the persistent experiment from the stateless output.
  • The create_context() call creates a new global namespace.
  • The contexts.append() call retains the context for cleanup.
  • The run_code() call creates score inside primary_context.
  • Save sandbox_lab.py.
  • Confirm the first stateful value by running this command:
python sandbox_lab.py

What Should You See Now?

After the stateless NameError, you should see [STDOUT] Primary score: 41.

The run finishes with Sandbox deleted..

Missing the Primary Score?

Confirm that context=primary_context appears inside the run_code() call. Check that each callback name matches the function defined at the top of the file.

Ask for help tracing the first stateful call in sandbox_lab.py.

  • Place the cursor below the first run_code() call.
  • Add the second stateful call by pasting this code:
        sandbox.code_interpreter.run_code(
            "score += 1\nprint(f'Updated primary score: {score}')",
            context=primary_context,
            on_stdout=handle_stdout,
            on_stderr=handle_stderr,
            on_error=handle_error,
            timeout=60,
        )

Why Does This Call Preserve State?

The second snippet uses the same primary_context. Its namespace already contains score = 41.

Incrementing that value produces visible proof that the context remembers state between calls.

  • Save sandbox_lab.py.
  • Confirm the score persists by running this command:
python sandbox_lab.py

What Proves State Persisted?

The stateful section should first print [STDOUT] Primary score: 41.

The next line should print [STDOUT] Updated primary score: 42.

That is the key breakthrough: score now survives between remote calls in primary_context.

Does the Updated Score Fail?

Confirm that both run_code() calls use context=primary_context. Check that the second snippet uses score += 1.

Ask for help comparing the two context calls in sandbox_lab.py.

Delete the tracked context

A persistent context remains a resource until deletion. Tracking it allows the finally block to clean it after success or failure.

  • Find if sandbox is not None: inside the finally block.
  • Place the cursor before the existing try block that deletes the sandbox.
  • Insert the context cleanup loop by pasting this code:
            for context in reversed(contexts):
                try:
                    sandbox.code_interpreter.delete_context(context)
                    print(f"Context deleted: {context.id}")
                except DaytonaError as cleanup_error:
                    print(f"Context cleanup failed: {cleanup_error}")

How Does Defensive Cleanup Work?

  • The reversed(contexts) loop processes the newest tracked context first.
  • The delete_context(context) call removes each user-created context.
  • The separate DaytonaError handler lets the sandbox deletion attempt continue after a context cleanup failure.
  • Save sandbox_lab.py.

Before you run the finished lab, do you expect the second stateful snippet to retain the value created by the first?

  • Run the completed stateful experiment with this command:
python sandbox_lab.py

What Confirms the Experiment Worked?

After the stateless NameError, you should see Primary score: 41. The next stateful line should be Updated primary score: 42.

The final lines should report the context deletion followed by Sandbox deleted..

Missing a Cleanup Message?

Confirm that contexts.append(primary_context) runs immediately after context creation. Check that the cleanup loop sits inside if sandbox is not None:.

Ask for help checking the context cleanup order in sandbox_lab.py.

✔️ Awesome, I've got everything!

Your stateful experiment now preserves score across calls. Your cleanup path also deletes the tracked context before deleting the sandbox.

ⓧ I'd like to double check the full code

Your complete sandbox_lab.py should match this reference.

from daytona import Daytona, DaytonaError, ExecutionError, OutputMessage


def handle_stdout(message: OutputMessage) -> None:
    print(f"[STDOUT] {message.output}", end="")


def handle_stderr(message: OutputMessage) -> None:
    print(f"[STDERR] {message.output}", end="")


def handle_error(error: ExecutionError) -> None:
    print(f"[ERROR] {error.name}: {error.value}")


def main() -> None:
    daytona = Daytona()
    sandbox = None
    contexts = []

    try:
        sandbox = daytona.create()
        print(f"Sandbox created: {sandbox.id}")

        print("\n1. Stateless execution")
        first_run = sandbox.process.code_run(
            "score = 41\nprint(f'First run score: {score}')",
            timeout=60,
        )
        print(first_run.result)

        second_run = sandbox.process.code_run(
            "print(f'Second run score: {score}')",
            timeout=60,
        )
        print(f"Exit code: {second_run.exit_code}")
        print(second_run.result)

        print("\n2. Persistent interpreter context")
        primary_context = sandbox.code_interpreter.create_context()
        contexts.append(primary_context)

        sandbox.code_interpreter.run_code(
            "score = 41\nprint(f'Primary score: {score}')",
            context=primary_context,
            on_stdout=handle_stdout,
            on_stderr=handle_stderr,
            on_error=handle_error,
            timeout=60,
        )
        sandbox.code_interpreter.run_code(
            "score += 1\nprint(f'Updated primary score: {score}')",
            context=primary_context,
            on_stdout=handle_stdout,
            on_stderr=handle_stderr,
            on_error=handle_error,
            timeout=60,
        )

    finally:
        if sandbox is not None:
            for context in reversed(contexts):
                try:
                    sandbox.code_interpreter.delete_context(context)
                    print(f"Context deleted: {context.id}")
                except DaytonaError as cleanup_error:
                    print(f"Context cleanup failed: {cleanup_error}")

            try:
                sandbox.delete(wait=True)
                print("Sandbox deleted.")
            except DaytonaError as cleanup_error:
                print(f"Sandbox cleanup failed: {cleanup_error}")


if __name__ == "__main__":
    main()

Your stateful path now carries score across calls. Next, a second context tests whether that state stays isolated.

Prove Context Isolation and Clean Up

Your Daytona lab now preserves score across two calls inside primary_context. That proves persistence inside one interpreter context.

A safe agent also needs context isolation. A second interpreter context must have its own global namespace.

You will test that boundary with score. You will then confirm that cleanup reaches every context plus the sandbox.

In this step, get ready to:
  • Create a second interpreter context to test namespace isolation.
  • List every user-created context before cleanup.
  • Confirm each context deletion is attempted before sandbox deletion.
Test a fresh context

The new context receives no setup code from primary_context. This creates a direct test for leaked state.

  • In sandbox_lab.py, find the second stateful sandbox.code_interpreter.run_code(...) call.
  • Add this block after that call:
        print("\n3. Isolated interpreter context")
        isolated_context = sandbox.code_interpreter.create_context()
        contexts.append(isolated_context)

        sandbox.code_interpreter.run_code(
            "print(score)",
            context=isolated_context,
            on_stdout=handle_stdout,
            on_stderr=handle_stderr,
            on_error=handle_error,
            timeout=60,
        )

What Does This Isolation Block Do?

  • The create_context() call creates a fresh execution namespace.
  • The contexts.append(isolated_context) line adds the new context to the existing cleanup list.
  • The context=isolated_context argument sends print(score) to the fresh context.
  • The callback arguments route interpreter output back to your terminal.
  • The timeout=60 argument limits the execution time.

Keep the Cleanup Path Intact

Running the lab creates a started Daytona sandbox. A started sandbox consumes free credit until it is deleted.

Your positive Free credit balance covers this short run. The existing finally block attempts deletion after the test.

  • Save sandbox_lab.py.

Before you run this, do you expect the fresh context to read score from primary_context?

  • Test the context boundary by running this command:
python sandbox_lab.py

You will see a line beginning with [ERROR] that identifies NameError. This is the intended proof that isolated_context cannot read variables from primary_context.

You will also see two context deletion messages. The final line is Sandbox deleted.

Does the Isolated Context Print a Score?

  • Check that the stateful call uses context=isolated_context.
  • Check that isolated_context comes from its own create_context() call.
  • Check that the new block sits outside the earlier primary_context calls.

Help me diagnose state leaking into isolated_context.

List the active contexts

An isolation error proves separate state. Listing the user-created contexts proves that both execution boundaries exist before cleanup begins.

  • In sandbox_lab.py, find the isolated run_code(...) block you just added.
  • Add this block directly below it:
        print("\nUser-created contexts:")
        for context in sandbox.code_interpreter.list_contexts():
            print(f"- {context.id}")

What Does the Context List Show?

  • The list_contexts() call returns user-created interpreter contexts.
  • The loop prints each context's id.
  • The two printed IDs provide visible evidence that primary_context plus isolated_context exist independently.
  • Save sandbox_lab.py.
  • Run the updated lab with this command:
python sandbox_lab.py

After User-created contexts:, you will see two lines containing different context IDs. The deletion messages follow those IDs.

Do You See Fewer Than Two Context IDs?

  • Check that both contexts.append(primary_context) plus contexts.append(isolated_context) remain in the file.
  • Check that the list_contexts() loop appears before finally.
  • Check that the loop body prints context.id.

Help me find why Daytona lists fewer than two user-created contexts.

Confirm defensive cleanup

Both contexts now live in the existing contexts list. The cleanup loop can attempt each deletion even if an earlier cleanup operation reports a problem.

  • Compare the existing finally block with this reference:
    finally:
        if sandbox is not None:
            for context in reversed(contexts):
                try:
                    sandbox.code_interpreter.delete_context(context)
                    print(f"Context deleted: {context.id}")
                except DaytonaError as cleanup_error:
                    print(f"Context cleanup failed: {cleanup_error}")

            try:
                sandbox.delete(wait=True)
                print("Sandbox deleted.")
            except DaytonaError as cleanup_error:
                print(f"Sandbox cleanup failed: {cleanup_error}")

How Does Defensive Cleanup Work?

  • The reversed(contexts) loop processes the newest context first.
  • Each delete_context(context) call has its own try block.
  • A DaytonaError from one context prints a cleanup failure.
  • The loop continues to the remaining tracked contexts.
  • The sandbox.delete(wait=True) call runs after context cleanup.
  • Double-check the completed sandbox_lab.py file with the tabs below.

✔️ Awesome, I've got everything!

Your file includes the isolated context test. It also lists both contexts before the existing cleanup path runs.

ⓧ I'd like to double check the full code

from daytona import Daytona, DaytonaError, ExecutionError, OutputMessage


def handle_stdout(message: OutputMessage) -> None:
    print(f"[STDOUT] {message.output}", end="")


def handle_stderr(message: OutputMessage) -> None:
    print(f"[STDERR] {message.output}", end="")


def handle_error(error: ExecutionError) -> None:
    print(f"[ERROR] {error.name}: {error.value}")


def main() -> None:
    daytona = Daytona()
    sandbox = None
    contexts = []

    try:
        sandbox = daytona.create()
        print(f"Sandbox created: {sandbox.id}")

        print("\n1. Stateless execution")
        first_run = sandbox.process.code_run(
            "score = 41\nprint(f'First run score: {score}')",
            timeout=60,
        )
        print(first_run.result)

        second_run = sandbox.process.code_run(
            "print(f'Second run score: {score}')",
            timeout=60,
        )
        print(f"Exit code: {second_run.exit_code}")
        print(second_run.result)

        print("\n2. Persistent interpreter context")
        primary_context = sandbox.code_interpreter.create_context()
        contexts.append(primary_context)

        sandbox.code_interpreter.run_code(
            "score = 41\nprint(f'Primary score: {score}')",
            context=primary_context,
            on_stdout=handle_stdout,
            on_stderr=handle_stderr,
            on_error=handle_error,
            timeout=60,
        )
        sandbox.code_interpreter.run_code(
            "score += 1\nprint(f'Updated primary score: {score}')",
            context=primary_context,
            on_stdout=handle_stdout,
            on_stderr=handle_stderr,
            on_error=handle_error,
            timeout=60,
        )

        print("\n3. Isolated interpreter context")
        isolated_context = sandbox.code_interpreter.create_context()
        contexts.append(isolated_context)

        sandbox.code_interpreter.run_code(
            "print(score)",
            context=isolated_context,
            on_stdout=handle_stdout,
            on_stderr=handle_stderr,
            on_error=handle_error,
            timeout=60,
        )

        print("\nUser-created contexts:")
        for context in sandbox.code_interpreter.list_contexts():
            print(f"- {context.id}")

    finally:
        if sandbox is not None:
            for context in reversed(contexts):
                try:
                    sandbox.code_interpreter.delete_context(context)
                    print(f"Context deleted: {context.id}")
                except DaytonaError as cleanup_error:
                    print(f"Context cleanup failed: {cleanup_error}")

            try:
                sandbox.delete(wait=True)
                print("Sandbox deleted.")
            except DaytonaError as cleanup_error:
                print(f"Sandbox cleanup failed: {cleanup_error}")


if __name__ == "__main__":
    main()

Before the final run, which lines do you expect to prove isolation? Which lines do you expect to prove cleanup?

  • Run the completed lab with this command:
python sandbox_lab.py

The isolated section reports NameError. Two context IDs appear under User-created contexts:.

Two Context deleted: messages follow. The final line is Sandbox deleted.

  • Return to the Daytona Dashboard from earlier.
  • Confirm that no sandbox from this run remains in the sandbox list.

Still Seeing a Sandbox?

  • Avoid starting another run while the previous sandbox remains.
  • Check the terminal for a sandbox cleanup failure.
  • Use the sandbox controls in the Daytona Dashboard to delete the remaining sandbox.
  • Confirm that the sandbox disappears from the list.

Help me diagnose why my Daytona sandbox remains after the finally block runs.

You have proved that one context keeps its state while another remains isolated. You have also proved that cleanup reaches every tracked context plus the sandbox.

Secret mission

Build a Fresh-Context Job Evaluator

Run two named code jobs through fresh Daytona interpreter contexts. The first job calculates a total. The second proves that no variable state leaked across jobs.

Clean Up Your Resources

Clean Up Your Resources

Your safety net held up: the completed run deleted the Daytona sandbox plus its interpreter contexts. No active sandbox billing remains while you choose whether to keep, pause, or delete the local lab.

Cost warning

A started sandbox bills reserved vCPU. It also bills reserved RAM.

Disk remains billable while a sandbox is started or stopped. A deleted sandbox is not billed for these resources.

Separately created snapshots can remain billable.

Resources you used:

  • The daytona-sandbox-lab folder in the active terminal. It contains requirements.txt, sandbox_lab.py, and the tutorial-env virtual environment.
  • The Daytona API key named daytona-sandbox-lab. It can authorize sandbox creation and deletion.

Keep everything running

No cleanup action is needed. Choose this if you plan to rerun or extend the evaluator soon.

  • Confirm the Daytona Dashboard shows no sandbox from the completed run.
  • Confirm the Daytona Dashboard shows no snapshot from this project.
  • Keep the daytona-sandbox-lab folder on your Mac.
  • Keep the Daytona API key named daytona-sandbox-lab active.

The active terminal remains the only shell holding DAYTONA_API_KEY.

Pause - I'll come back to this later

Revoking the key is deliberate. It blocks new sandbox creation through that credential while leaving your local files intact.

  • Locate the key named daytona-sandbox-lab in the Daytona API key area.
  • Click Revoke next to the key.
  • Confirm the revocation.

Access through that key ends immediately. Your local lab remains ready for a replacement key later.

  • Close the active terminal.
  • Confirm the Daytona Dashboard shows no remaining sandbox.
  • Confirm the Daytona Dashboard shows no remaining snapshot from this project.

The tutorial-env session ends when the terminal closes. The exported API key leaves that shell with it.

Delete - I don't want to use this again

This cleanup permanently removes your local lab. Your Daytona account remains available for future projects.

  • Locate the key named daytona-sandbox-lab in the Daytona API key area.
  • Click Revoke next to the key.
  • Confirm the revocation.
  • Confirm the Daytona Dashboard shows no remaining sandbox.
  • Confirm the Daytona Dashboard shows no remaining snapshot from this project.

The key can no longer authorize another sandbox. The final local cleanup removes your code plus its virtual environment.

  • Remove the daytona-sandbox-lab folder from its parent directory by running these commands in the active terminal:
cd ..
rm -rf daytona-sandbox-lab

What does this cleanup remove?

The first command moves the terminal to the folder that contains your project. The second command permanently removes the lab folder.

This removes requirements.txt, sandbox_lab.py, and tutorial-env.

  • Confirm daytona-sandbox-lab is absent from its former parent folder in Finder.

Still see the project folder?

Check that the active terminal started inside daytona-sandbox-lab. The first command relies on that starting location.

  • Remove daytona-sandbox-lab from its actual parent folder in Finder if you moved it earlier.
  • Check the folder permissions in Finder if macOS blocks the deletion.

Help me safely remove the daytona-sandbox-lab folder from my Mac.

Nice Work!

Nice Work!

Mission complete! Your Python lab now controls a Daytona sandbox from creation through deletion.

You've learned how to:

  • Run Python remotely through stateless execution to reveal when data disappears between independent calls.
  • Prove state persistence inside primary_context. Prove context isolation with isolated_context.
  • Capture remote output through callbacks. Report interpreter errors without exposing your API key. Guarantee defensive cleanup from a finally block.
  • Secret Mission: Build a fresh-context evaluator that runs every code job in a new interpreter context. Delete each job context after execution.

Ready to quiz yourself?