---
title: "Migrate to Stream Mode"
description: "Learn how to migrate your custom instrumentation from transaction mode to stream mode."
url: https://docs.sentry.io/platforms/python/tracing/streamed-spans/migration-guide/
---

# Migrate to Stream Mode for Python (SDK v3.x)

Version 3.0 of the Python SDK is coming in November 2026. This guide describes upcoming behavior. For the SDK 2.x version of this guide, select 2.x in the version selector in the sidebar.

Starting with SDK 3.0, stream mode is the only tracing mode: all spans are streamed, and the transaction-based Span API no longer exists. If you use custom instrumentation (creating spans manually, setting span data, or filtering spans) you'll need to update that code when upgrading. This guide walks through the changes.

For an introduction to stream mode itself, see [Streamed Spans](https://docs.sentry.io/platforms/python/tracing/streamed-spans.md).

## [Remove `trace_lifecycle`](https://docs.sentry.io/platforms/python/tracing/streamed-spans/migration-guide.md#remove-trace_lifecycle)

The `trace_lifecycle` option no longer exists. If you enabled stream mode on SDK 2.x, remove the option, since passing an unknown option to `sentry_sdk.init()` raises a `TypeError`:

```python
import sentry_sdk

sentry_sdk.init(
    dsn="https://<key>@o<orgId>.ingest.sentry.io/<projectId>",
    traces_sample_rate=1.0,
-   trace_lifecycle="stream",
)
```

If you already migrated to the `sentry_sdk.traces` API on SDK 2.x, your code keeps working. The top-level API (`sentry_sdk.start_span()`, `sentry_sdk.get_current_span()`, and so on) now uses the same streamed implementation, so we recommend switching to it.

## [Span Creation](https://docs.sentry.io/platforms/python/tracing/streamed-spans/migration-guide.md#span-creation)

Replace `start_transaction` and `start_child` with `sentry_sdk.start_span()`. Whether the resulting span is a service span, a child span, or a sibling depends on the `parent_span` argument and what's currently active.

```python
import sentry_sdk

# Starting a span
- with sentry_sdk.start_span(op="http.client", description="GET /api/users") as span:
+ with sentry_sdk.start_span(
+     name="GET /api/users",
+     attributes={"sentry.op": "http.client"},
+ ) as span:
      ...

# Starting what used to be a transaction: pass parent_span=None to force a service span
- with sentry_sdk.start_transaction(name="flow.checkout") as transaction:
+ with sentry_sdk.start_span(name="flow.checkout", parent_span=None) as span:
      ...

# Starting a child span: just start a span while the parent is active
- with parent.start_child(op="db", description="SELECT") as child:
+ with sentry_sdk.start_span(name="SELECT", attributes={"sentry.op": "db"}):
      ...

# Starting a child span: if the span's parent should be a span that's currently not active, you can provide it explicitly
- with parent.start_child(op="db", description="SELECT") as child:
+ with sentry_sdk.start_span(name="SELECT", attributes={"sentry.op": "db"}, parent_span=parent):
      ...
```

A few argument changes come along with this:

* `name` is now required.
* `description` no longer exists — use `name` instead.
* `op` is no longer a dedicated argument — set it as the `sentry.op` attribute instead.

If you use the `@trace` decorator, the import stays the same. The decorator accepts optional `name`, `attributes`, and `active` arguments. `op` and `template` no longer exist — set `sentry.op` as an attribute instead:

```python
from sentry_sdk import trace

- @trace(op="checkout")
+ @trace(attributes={"sentry.op": "checkout"})
def checkout():
    ...
```

The `sentry_sdk.tracing` module has been removed. If your code imports `Span` or `Transaction` from it, for example for type annotations, replace both with `Span` from `sentry_sdk.traces`:

```python
- from sentry_sdk.tracing import Span, Transaction
+ from sentry_sdk.traces import Span

- def process(transaction: Transaction) -> None:
+ def process(span: Span) -> None:
      ...
```

## [Span Data](https://docs.sentry.io/platforms/python/tracing/streamed-spans/migration-guide.md#span-data)

In stream mode, spans have no contexts, data, or tags. Instead, everything is an attribute. Replace `set_data()`, `set_tag()`, and `set_context()` with `set_attribute()` or `set_attributes()`:

```python
- span.set_data("flow.step", "submit_payment")
- span.set_tag("http.status_code", 201)
+ span.set_attributes({
+     "flow.step": "submit_payment",
+     "http.response.status_code": 201,
+ })
```

Unlike the old methods, `set_attribute` only accepts primitive types (`str`, `int`, `float`, `bool`, or arrays of these). `None` isn't supported either. Flatten dictionaries into separate attributes, and stringify anything that can't be flattened:

```python
- span.set_data("request", {"method": "POST", "path": "/api/checkout"})
+ span.set_attributes({
+     "request.method": "POST",
+     "request.path": "/api/checkout",
+ })
```

Tags set on the scope with `sentry_sdk.set_tag()` aren't applied to spans. Use `sentry_sdk.set_attribute()` to apply data to spans:

```python
import sentry_sdk

sentry_sdk.set_tag("region", "Europe")       # applied to errors and other tag-supporting telemetry
sentry_sdk.set_attribute("region", "Europe")  # applied to spans, logs, metrics
```

## [Accessing the Current Span](https://docs.sentry.io/platforms/python/tracing/streamed-spans/migration-guide.md#accessing-the-current-span)

`sentry_sdk.get_current_span()` works the same way, but returns a streamed span. Prefer it over reading the span off the scope:

```python
import sentry_sdk

- scope = sentry_sdk.get_current_scope()
- current_span = scope.span
+ current_span = sentry_sdk.get_current_span()
```

If your code reads specific fields off the trace context, access them as direct properties instead of calling `get_trace_context()`, which no longer exists:

```python
- ctx = span.get_trace_context()
- trace_id = ctx["trace_id"]
- span_id = ctx["span_id"]
+ trace_id = span.trace_id
+ span_id = span.span_id
```

## [Trace Propagation](https://docs.sentry.io/platforms/python/tracing/streamed-spans/migration-guide.md#trace-propagation)

`sentry_sdk.continue_trace()` now accepts a single dictionary with the incoming headers. It's no longer a context manager and doesn't return a transaction — it sets the propagation context, and the next span you start picks it up automatically:

```python
import sentry_sdk

headers = {
    "sentry-trace": "...",
    "baggage": "...",
}

- with sentry_sdk.continue_trace(headers) as transaction:
-     ...
+ sentry_sdk.continue_trace(headers)
+ with sentry_sdk.start_span(name="handle request"):
+     ...
```

To start a new trace that's unconnected to the current one, use `sentry_sdk.new_trace()`. Like `continue_trace()`, it doesn't start a span on its own.

## [Span Status](https://docs.sentry.io/platforms/python/tracing/streamed-spans/migration-guide.md#span-status)

Status can only be `ok` (default) or `error`:

```python
import sentry_sdk

with sentry_sdk.start_span(name="process") as span:
    try:
        ...
    except Exception:
        span.status = "error"
```

## [Sampling](https://docs.sentry.io/platforms/python/tracing/streamed-spans/migration-guide.md#sampling)

If you use `traces_sample_rate`, no changes are needed.

If you use a custom `traces_sampler`, the sampling context has the same structure as before, but not all keys might be populated. The `data` key under `transaction_context` contains span attributes known at span start.

```python
import sentry_sdk

def traces_sampler(sampling_context):
    if sampling_context["parent_sampled"] is not None:
        return float(sampling_context["parent_sampled"])

    if sampling_context["transaction_context"]["name"] in IGNORED_SPAN_NAMES:
        return 0.0

    return 1.0

sentry_sdk.init(
    traces_sampler=traces_sampler,
)
```

`custom_sampling_context` is no longer an argument to `start_span`. Set it on the scope instead, after `continue_trace` (which resets the propagation context) and before `start_span` (which is when sampling happens):

```python
import sentry_sdk

- with sentry_sdk.start_span(
-     name="handle request",
-     custom_sampling_context={"asgi_scope": asgi_scope},
- ):
-     ...
+ sentry_sdk.get_current_scope().set_custom_sampling_context({"asgi_scope": asgi_scope})
+ with sentry_sdk.start_span(name="handle request"):
+     ...
```

## [Filtering and Dropping Spans](https://docs.sentry.io/platforms/python/tracing/streamed-spans/migration-guide.md#filtering-and-dropping-spans)

The `before_send_transaction` option has been removed, since spans are sent individually rather than batched into a transaction. Replace it with `ignore_spans` (to drop spans) and `before_send_span` (to modify them). If you previously set either of these under `_experiments`, move them to the top level:

```python
import re
import sentry_sdk

+ def my_span_processor(span, hint):
+     if span["attributes"].get("sentry.op") == "db.query":
+         span["name"] = "[filtered]"
+     return span

sentry_sdk.init(
    dsn="https://<key>@o<orgId>.ingest.sentry.io/<projectId>",
    traces_sample_rate=1.0,
-   before_send_transaction=my_filter,
+   ignore_spans=[
+       "/health",
+       re.compile(r"^GET /api/v1/internal"),
+       {"attributes": {"service.id": "15def9a"}},
+   ],
+   before_send_span=my_span_processor,
)
```

`ignore_spans` only has access to the span name and attributes set at creation time — not attributes added later in the span's lifetime, like an HTTP status code set after the request completes. If your `before_send_transaction` logic was used to drop spans based on late-set data, this can't be replicated in stream mode. Consider server-side filtering with Sentry [inbound data filters](https://docs.sentry.io/concepts/data-management/filtering.md) or [Relay](https://docs.sentry.io/product/relay.md) rules instead. `before_send_span` runs after the span ends and has access to all attributes set during the span's lifetime.
