# Temporal Python SDK sandbox environment

> The Temporal Python SDK offers a sandbox environment to run Workflow code, aiming to prevent non-determinism errors in applications by isolating global state and applying restrictions.

The Temporal Python SDK enables you to run Workflow code in a sandbox environment to help prevent non-determinism errors in your application.
The Temporal Workflow Sandbox for Python is not completely isolated, and some libraries can internally mutate state, which can result in breaking determinism.

## Benefits

Temporal's Python SDK uses a sandbox environment for Workflow runs to make developing Workflow code safer.

If a Workflow Execution performs a non-deterministic event, an exception is thrown, which results in failing the Task Worker.
The Workflow will not progress until the code is fixed.

The Temporal Python sandbox offers a mechanism to _pass through modules_ from outside the sandbox. By default, this includes all standard library modules and Temporal modules. For performance and behavior reasons, users should pass through all models, Activities, Nexus services, or other modules that are in separate files whose calls will be deterministic. For more information, see [Passthrough modules](#passthrough-modules).

## How it works

The Sandbox environment consists of two main components.

- [Global state isolation](#global-state-isolation)
- [Restrictions](#restrictions)

### Global state isolation

The first component of the Sandbox is a global state isolation.
Global state isolation uses `exec` to compile and evaluate statements.

Upon the start of a Workflow, the file in which the Workflow is defined is imported into a newly created sandbox.

If a module is imported by the file, a known set, which includes all of Python's standard library, is _passed through_ from outside the sandbox.

These modules are expected to be free of side effects and have their non-deterministic aspects restricted.

For a full list of modules imported, see [Customize the Sandbox](#customize-the-sandbox).

### Restrictions

Restrictions prevent known non-deterministic library calls.
This is achieved by using proxy objects on modules wrapped around the custom importer set in the sandbox.

Restrictions apply at both the Workflow import level and the Workflow run time.

A default set of restrictions that prevents most dangerous standard library calls.

## Skip Workflow Sandboxing

The following techniques aren't recommended, but they allow you to avoid, skip, or break through the sandbox environment.

Skipping Workflow Sandboxing results in a lack of determinism checks. Using the Workflow Sandboxing environment helps prevent non-determinism errors but doesn't completely negate the risk.

### Skip Sandboxing for a block of code

To skip a sandbox environment for a specific block of code in a Workflow, use [`sandbox_unrestricted()`](https://python.temporal.io/temporalio.workflow.unsafe.html#sandbox_unrestricted). The Workflow will run without sandbox restrictions.

```python
with temporalio.workflow.unsafe.sandbox_unrestricted():
    # Your code
```

### Skip Sandboxing for an entire Workflow

To skip a sandbox environment for a Workflow, set the `sandboxed` argument in the [`@workflow.defn`](https://python.temporal.io/temporalio.workflow.html#defn) decorator to false.
The entire Workflow will run without sandbox restrictions.

```python
@workflow.defn(sandboxed=False)
```

### Skip Sandboxing for a Worker

To skip a sandbox environment for a Worker, set the `workflow_runner` keyword argument of the `Worker` init to [`UnsandboxedWorkflowRunner()`](https://python.temporal.io/temporalio.worker.UnsandboxedWorkflowRunner.html).

## Customize the sandbox

When creating the Worker, the `workflow_runner` defaults to [`SandboxedWorkflowRunner()`](https://python.temporal.io/temporalio.worker.workflow_sandbox.SandboxedWorkflowRunner.html).
The `SandboxedWorkflowRunner` init accepts a `restrictions` keyword argument that defines a set of restrictions to apply to this sandbox.

The [`SandboxRestrictions`](https://python.temporal.io/temporalio.worker.workflow_sandbox.SandboxRestrictions.html) dataclass is immutable and contains four fields that can be customized, but only three have notable values.

- [`passthrough_modules`](https://python.temporal.io/temporalio.worker.workflow_sandbox.SandboxRestrictions.html#passthrough_modules)
- [`invalid_modules_members`](https://python.temporal.io/temporalio.worker.workflow_sandbox.SandboxRestrictions.html#invalid_module_members)
- [`import_notification_policy`](https://python.temporal.io/temporalio.worker.workflow_sandbox.SandboxRestrictions.html#import_notificaton_policy)

### Passthrough modules

By default, the sandbox completely reloads non-standard-library and non-Temporal modules for every Workflow run. Passing through a module means that the module will not be reloaded every time the Workflow runs. Instead, the module will be imported from outside the sandbox and used directly in the Workflow. This can improve performance because importing a module can be a time-consuming process, and passing through a module can avoid this overhead.

> **📝 Note:**
> It is important to note that you should only import _known-side-effect-free_ third-party modules: meaning they don't have any unintended consequences when imported and used multiple times. This is because passing through a module means that it will be used multiple times in a Workflow without being reloaded, so any side effects it has won't be repeated. For this reason, it's recommended to only pass through modules that are known to be deterministic, meaning they will always produce the same output given the same input.

One way to pass through a module is at import time in the Workflow file using the [`imports_passed_through`](https://python.temporal.io/temporalio.workflow.unsafe.html#imports_passed_through) context manager.

```python
# my_workflow_file.py

from temporalio import workflow

with workflow.unsafe.imports_passed_through():
    import pydantic

@workflow.defn
class MyWorkflow:
     # ...
```

Alternatively, this can be done at Worker creation time by customizing the runner's restrictions.

```python
# my_worker_file.py

from temporalio.worker import Worker
from temporalio.worker.workflow_sandbox import SandboxedWorkflowRunner, SandboxRestrictions

my_worker = Worker(
  ...,
  workflow_runner=SandboxedWorkflowRunner(
    restrictions=SandboxRestrictions.default.with_passthrough_modules("pydantic")
  )
)
```

In both of these cases, now the `pydantic` module will be passed through from outside the sandbox instead of being reloaded for every Workflow run.

### Invalid module members

`invalid_module_members` includes modules that cannot be accessed.

Checks are compared against the fully qualified path to the item.

For example, to remove a restriction on `datetime.date.today()`, see the following example.

```python
# my_worker_file.py

import dataclasses
from temporalio.worker import Worker
from temporalio.worker.workflow_sandbox import SandboxedWorkflowRunner, SandboxRestrictions

my_restrictions = dataclasses.replace(
    SandboxRestrictions.default,
    invalid_module_members=SandboxRestrictions.invalid_module_members_default.with_child_unrestricted(
      "datetime", "date", "today",
    ),
)
my_worker = Worker(..., workflow_runner=SandboxedWorkflowRunner(restrictions=my_restrictions))
```

Restrictions can also be added by piping (`|`) together [`SandboxMatcher`](https://python.temporal.io/temporalio.worker.workflow_sandbox.SandboxMatcher.html) instances.

The following example restricts the `datetime.date` class from being used.

```python
# my_worker_file.py

import dataclasses
from temporalio.worker import Worker
from temporalio.worker.workflow_sandbox import (
  SandboxedWorkflowRunner,
  SandboxMatcher,
  SandboxRestrictions,
)

my_restrictions = dataclasses.replace(
    SandboxRestrictions.default,
    invalid_module_members=SandboxRestrictions.invalid_module_members_default | SandboxMatcher(
      children={"datetime": SandboxMatcher(use={"date"})},
    ),
)
my_worker = Worker(..., workflow_runner=SandboxedWorkflowRunner(restrictions=my_restrictions))
```

### Import Notification Policy

The sandbox's import notification policy specifies how the sandbox behaves when it imports modules in a way that may be unintentional. It covers two common scenarios: dynamic imports and enforcing module passthrough. Each can be controlled independently.

A dynamic import occurs when a module is imported after the Workflow is loaded into the sandbox. These imports are often invisible and, if they don't do anything restricted by the sandbox, cause memory overhead. By default the [`WARN_ON_DYNAMIC_IMPORT`](https://python.temporal.io/temporalio.workflow.SandboxImportNotificationPolicy.html#WARN_ON_DYNAMIC_IMPORT) policy setting is enabled and a warning will be emitted when a module that is not in the [passthrough modules](#passthrough-modules) list is dynamically imported.

The other notable policy settings apply when a module is imported into the sandbox that was not passed through. These settings are disabled by default and must be explicitly turned on.
The [`WARN_ON_UNINTENTIONAL_PASSTHROUGH`](https://python.temporal.io/temporalio.workflow.SandboxImportNotificationPolicy.html#WARN_ON_UNINTENTIONAL_PASSTHROUGH) setting emits a warning when a module not included in the [passthrough modules](#passthrough-modules) list is imported.

Similarly, the [`RAISE_ON_UNINTENTIONAL_PASSTHROUGH`](https://python.temporal.io/temporalio.workflow.SandboxImportNotificationPolicy.html#RAISE_ON_UNINTENTIONAL_PASSTHROUGH) setting will raise an error when a non-passed-through module is imported.

The import notification policy can be set for specific imports by using [`sandbox_import_notification_policy`](https://python.temporal.io/temporalio.workflow.unsafe.html#sandbox_import_notification_policy) context manager.

```python
# my_workflow_file.py

from temporalio import workflow

with workflow.unsafe.sandbox_import_notification_policy(
    workflow.SandboxImportNotificationPolicy.SILENT
):
    import pydantic

@workflow.defn
class MyWorkflow:
     # ...
```

This can also be done at worker creation time by customizing the runner's restrictions.

```python
# my_worker_file.py

from temporalio.worker import Worker
from temporalio.worker.workflow_sandbox import SandboxedWorkflowRunner, SandboxRestrictions

my_worker = Worker(
  ...,
  workflow_runner=SandboxedWorkflowRunner(
    restrictions=SandboxRestrictions.default.with_import_notification_policy( 
      workflow.SandboxImportNotificationPolicy.WARN_ON_DYNAMIC_IMPORT | workflow.SandboxImportNotificationPolicy.WARN_ON_UNINTENTIONAL_PASSTHROUGH
    )
  )
)
```

The [`sandbox_import_notification_policy`](https://python.temporal.io/temporalio.workflow.unsafe.html#sandbox_import_notification_policy) context manager will always be respected if used in combination with the restrictions customization. 

For more information on the Python sandbox, see the following resources.

- [Python SDK README](https://github.com/temporalio/sdk-python)
- [Python API docs](https://python.temporal.io/index.html)
