Files
e2b-dev--e2b/packages/python-sdk/README.md
T
devin-ai-integration[bot] 5759f17e56 feat(sdk): add E2B client for multiple bound connection configs (#1720)
## Summary

Adds an `E2B` client to both SDKs so a process can talk to several API
keys / domains / deployments without going through environment
variables. The client binds a connection config once and exposes the
resource surfaces off it; the named top-level exports are untouched and
keep reading the environment.

Nothing existing changes (changeset is `minor`): the default export is
still `Sandbox`, `Template(...)` keeps working, and `E2B` is a new named
export. Two follow-ups are tracked for v3: making `E2B` the default
export
([SDK-341](https://linear.app/e2b/issue/SDK-341/sdk-v3-js-make-e2b-the-default-export-instead-of-sandbox))
and dropping the `Template` Proxy in favour of `new Template()`
([SDK-342](https://linear.app/e2b/issue/SDK-342/sdk-v3-js-drop-the-template-proxy-require-new-template)).

```ts
import { E2B } from 'e2b'

const { Sandbox, Volume, Template, Secret } = new E2B({
  apiKey: 'e2b_***',
  domain: 'e2b.dev',
})

const sandbox = await Sandbox.create()
const volume = await Volume.create('my-volume')
const exists = await Template.exists('my-template')
await Template.build(Template().fromPythonImage('3'), 'my-env')
await Secret.create('openai-api-key', 'sk-***')

// Per-call options still win over the client's options.
await Sandbox.create({ apiKey: 'e2b_other***' })
```

```python
from e2b import E2B

client = E2B(api_key="e2b_***", domain="e2b.dev")
Sandbox, Volume, Template = client.Sandbox, client.Volume, client.Template
Secret = client.Secret

sandbox = Sandbox.create()
volume = Volume.create("my-volume")
exists = Template.exists("my-template")
secret = Secret.create("openai-api-key", "sk-***")

# Async variants are exposed too.
AsyncSandbox, AsyncTemplate = client.AsyncSandbox, client.AsyncTemplate
async_sandbox = await AsyncSandbox.create()
await AsyncTemplate.exists("my-template")
```

### Mechanism

`client.Sandbox` / `client.Volume` / `client.Template` / `client.Secret`
(plus the `Async*` variants in Python) are per-client subclasses of the
real classes, carrying the bound opts as class-level state. Nothing
process-global is mutated, so clients are isolated from each other and
from the default path, and `cls`/`this` dispatch is preserved (`create`
on a client class returns an instance of that client class).

```ts
// sandboxApi.ts / volume/index.ts / template/index.ts / secret.ts — one hook per class hierarchy
protected static readonly boundOpts?: ConnectionOpts // undefined on the base classes
protected static resolveOpts<T extends ConnectionOpts>(opts?: T) {
  return ConnectionConfig.mergeOpts(this.boundOpts, opts) // { ...bound, ...definedPerCall }
}

// every static method that built a config from raw opts now does
- const config = new ConnectionConfig(opts)
+ const apiOpts = this.resolveOpts(opts)
+ const config = new ConnectionConfig(apiOpts)
```

```py
# sandbox/main.py, volume_sync.py, volume_async.py, template_{sync,async}/main.py, secret/base.py
_bound_api_params: ApiParams = {}  # empty on the base classes

@classmethod
def _resolve_api_params(cls, **opts: Unpack[ApiParams]) -> ApiParams:
    return merge_api_params(cls._bound_api_params, opts)

- config = ConnectionConfig(**opts)
+ config = ConnectionConfig(**cls._resolve_api_params(**opts))
```

`Template` used to be a factory function whose statics were pre-bound to
`TemplateBase`, which left no class for a client to subclass. It is now
the `TemplateBase` class itself, wrapped in a `Proxy` whose only trap
makes it callable without `new`, so `Template(...)` keeps working (no
breaking change) while `client.Template` is a plain subclass like
Sandbox/Volume and `Template.build(...)` resolves `this` naturally:

```ts
export function callableTemplate<T extends typeof TemplateBase>(cls: T) {
  return new Proxy(cls, { apply: (target, _this, args) => new target(...args) })
}
export const Template = callableTemplate(TemplateBase)          // Template() still returns a builder
this.Template = callableTemplate(class extends TemplateBase { boundOpts })  // client.Template
```

Because the trap only intercepts calls, `new Template()`, statics,
`instanceof` and subclassing all go straight to the class, and the
builder's default file context (`getCallerDirectory()`) still resolves
to the user's frame (the trap's frame is inside the SDK and filtered
like the old factory's).

Two side effects of routing everything through the hook:

- Static methods that resolved config off the base class had to move to
`this`/`cls`: `SandboxApi.createSandbox(...)` →
`this.createSandbox(...)` in JS, `new Volume(...)` → `new this(...)`,
and several Python `@staticmethod`s (`SandboxApi.list`,
`_cls_list_snapshots`, `delete_snapshot`, `Volume._class_get_info` /
`_class_list` / `destroy`, and the `Secret` operations) became
`@classmethod`s. Behavior for the top-level classes is unchanged since
their bound opts are empty.
- `DualMethod.__get__` (the descriptor behind `Volume.get_info` /
`Volume.list` working both on the class and on instances) now binds the
class-level function to the accessed class, so `client.Volume.list()`
sees the subclass' bound params instead of `Volume`'s.

Per-call values explicitly set to `undefined` / `None` are dropped when
merging, so they fall back to the client's opts rather than clearing
them into the env-var path.

### Tests

`packages/js-sdk/tests/client.test.ts` (MSW) and
`packages/python-sdk/tests/test_client.py` (local HTTP server, sync +
async) cover: the client's API key/domain being used instead of the env
vars, per-call precedence, rebinding the class (`const S =
client.Sandbox`), rebound `client.Template`, the client template builder
producing the same Dockerfile as the top-level one, two clients staying
isolated, generated-subclass instances, `client.Secret` (sync + async)
using the bound config, the top-level classes still using the env config
with empty bound opts and the default export still being `Sandbox`.



Link to Devin session:
https://app.devin.ai/sessions/772afa048b814ad784b5dde0a599df46
Requested by: @mishushakov

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: mish@e2b.dev <mish@e2b.dev>
Co-authored-by: Mish Ushakov <10400064+mishushakov@users.noreply.github.com>
2026-08-20 18:03:15 +00:00

3.6 KiB

E2B Logo

Last 1 month downloads for the Python SDK

What is E2B?

E2B is an open-source infrastructure that allows you to run AI-generated code in secure isolated sandboxes in the cloud. To start and control sandboxes, use our JavaScript SDK or Python SDK.

Run your first Sandbox

1. Install SDK

pip install e2b

2. Get your E2B API key

  1. Sign up to E2B here.
  2. Get your API key here.
  3. Set environment variable with your API key
E2B_API_KEY=e2b_***

3. Start a sandbox and run commands

from e2b import Sandbox

with Sandbox.create() as sandbox:
    result = sandbox.commands.run('echo "Hello from E2B!"')
    print(result.stdout)  # Hello from E2B!

4. Bind the configuration to a client

The top-level Sandbox, AsyncSandbox, Volume, AsyncVolume, Template, AsyncTemplate and Secret exports read their configuration from the environment variables. To use an explicit configuration — e.g. several API keys or domains in one process — create an E2B client and use the resource classes it exposes:

from e2b import E2B

client = E2B(api_key="e2b_***", domain="e2b.dev")

sandbox = client.Sandbox.create()
volume = client.Volume.create("my-volume")
exists = client.Template.exists("my-template")
secret = client.Secret.create("openai-api-key", "sk-***")

# The async variants are exposed as well.
async_sandbox = await client.AsyncSandbox.create()

# The classes can be assigned and used like the top-level ones.
Sandbox = client.Sandbox
paginator = Sandbox.list()

Per-call params still take precedence over the client's params, and clients are isolated from each other and from the env-configured top-level exports.

5. Code execution with Code Interpreter

If you need run_code(), install the Code Interpreter SDK:

pip install e2b-code-interpreter
from e2b_code_interpreter import Sandbox

with Sandbox.create() as sandbox:
    execution = sandbox.run_code("x = 1; x += 1; x")
    print(execution.text)  # outputs 2

6. Check docs

Visit E2B documentation.

7. E2B cookbook

Visit our Cookbook to get inspired by examples with different LLMs and AI frameworks.