---
title: "Python v3 → v4"
description: Migration guide for upgrading the Langfuse Python SDK from v3 to v4.
category: SDKs
---

# Python v3 → v4

The Python SDK v4 introduces the **[observations-first data model](/docs/observability/data-model)**. In this model, correlating attributes (`user_id`, `session_id`, `metadata`, `tags`) propagate to every observation rather than living only on the trace. This enables single-table queries without expensive joins, significantly improving query performance at scale.

This changes how you set trace attributes: instead of imperatively updating the trace object with `update_current_trace()`, you use `propagate_attributes()` — a context manager that automatically applies attributes to the current and all child observations created within its scope.

  v4 changes default OpenTelemetry export behavior: Langfuse no longer exports all
  spans by default. If you previously relied on non-LLM spans (HTTP, DB, queue,
  framework internals) being forwarded, review the first breaking change below
  before upgrading.

## Breaking Changes

### Smart default span filtering replaces export-all behavior

In previous versions, exporting all OpenTelemetry spans by default increased trace noise from infrastructure and non-LLM instrumentation (HTTP, DB, queues, framework internals). To keep traces focused and useful, v4 introduces a smart default span filter.

By default, v4 exports a span if any of these are true:

- The span was created by Langfuse (`langfuse-sdk`)
- The span has `gen_ai.*` attributes
- The span instrumentation scope matches known LLM scope prefixes (for example `openinference`, `langsmith`, `haystack`, `litellm`)

Before v4, non-blocked instrumentation scopes were exported by default.

#### Keep pre-v4 "export everything" behavior

```python
from langfuse import Langfuse

langfuse = Langfuse(should_export_span=lambda span: True)
```

#### Compose custom filters with default behavior

```python
from langfuse import Langfuse
from langfuse.span_filter import is_default_export_span

langfuse = Langfuse(
    should_export_span=lambda span: (
        is_default_export_span(span)
        or (
            span.instrumentation_scope is not None
            and span.instrumentation_scope.name.startswith("my_framework")
        )
    )
)
```

#### Python compatibility note: `blocked_instrumentation_scopes` is deprecated

`blocked_instrumentation_scopes` still works in v4, but is deprecated and will be removed in a future version. Migrate to `should_export_span`.

Equivalent denylist behavior with `should_export_span`:

```python
from langfuse import Langfuse
from langfuse.span_filter import is_default_export_span

blocked = {"sqlite", "requests"}

langfuse = Langfuse(
    should_export_span=lambda span: (
        is_default_export_span(span)
        and (
            span.instrumentation_scope is None
            or span.instrumentation_scope.name not in blocked
        )
    )
)
```

If both `blocked_instrumentation_scopes` and `should_export_span` are set, blocked scopes still win (hard veto).

#### Possible trace-tree side effects and how to debug

Filtering can break trace trees when intermediate or parent spans are dropped while child spans are still exported. If traces appear disconnected, enable SDK debug logging to inspect dropped spans, then allowlist the required scopes in your callback.

- Python debug mode: use `Langfuse(debug=True)` or set `LANGFUSE_DEBUG="True"`.
- See [SDK advanced features](/docs/observability/sdk/advanced-features) and [OpenTelemetry troubleshooting for unwanted spans](/faq/all/existing-otel-setup#unwanted-spans-in-langfuse).

### `update_current_trace()` decomposed into 3 methods

In the new model, correlating attributes (`user_id`, `session_id`, `metadata`, `tags`, and request-scoped `environment`) must live on every observation, not just the trace. This is why they move to `propagate_attributes()` — a context manager that automatically applies these attributes to the current and all child observations created within its scope.

**v3:**

```python
langfuse.update_current_trace(
    name="trace-name",
    user_id="user-123",
    session_id="session-abc",
    version="1.0",
    input={"query": "hello"},
    output={"result": "world"},
    metadata={"key": "value"},
    tags=["tag1"],
    public=True,
)
```

**v4 (decomposed):**

```python
from langfuse import observe, propagate_attributes, get_client

langfuse = get_client()

@observe()
def my_function():
    # (a) Correlating attributes → propagate_attributes() context manager
    with propagate_attributes(
        trace_name="trace-name",  # note: 'name' is now 'trace_name'
        user_id="user-123",
        session_id="session-abc",
        version="1.0",
        metadata={"key": "value"},
        tags=["tag1"],
        environment="staging",
    ):
        result = call_llm("hello")

    # (b) Trace I/O (deprecated, only for legacy trace-level LLM-as-a-judge configurations)
    langfuse.set_current_trace_io(input={"query": "hello"}, output={"result": result})

    # (c) Public flag
    langfuse.set_current_trace_as_public()
```

**Key differences:**

| Attribute                                  | v3                                      | v4                                                                                                                                                                             |
| ------------------------------------------ | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `name`                                     | `update_current_trace(name=...)`        | `propagate_attributes(trace_name=...)`                                                                                                                                         |
| `user_id`, `session_id`, `tags`, `version` | `update_current_trace(...)`             | `propagate_attributes(...)`                                                                                                                                                    |
| `metadata`                                 | `update_current_trace(metadata=any)`    | `propagate_attributes(metadata=dict[str,str])`                                                                                                                                 |
| `input`, `output`                          | `update_current_trace(...)`             | `set_current_trace_io(...)` (deprecated)                                                                                                                                       |
| `public`                                   | `update_current_trace(public=True)`     | `set_current_trace_as_public()`                                                                                                                                                |
| `release`                                  | `update_current_trace(release=...)`     | Removed — use `LANGFUSE_RELEASE` env var                                                                                                                                       |
| `environment`                              | `update_current_trace(environment=...)` | Use `LANGFUSE_TRACING_ENVIRONMENT` or `Langfuse(environment=...)` for process-level environments. Use `propagate_attributes(environment=...)` for request-scoped environments. |

  `set_current_trace_io()` is deprecated and exists only for backward
  compatibility with trace-level
  [LLM-as-a-judge](/docs/evaluation/evaluation-methods/llm-as-a-judge)
  evaluators that rely on trace input/output. For new code, set input/output on
  the root observation directly.

### `span.update_trace()` decomposed into 3 methods

The same decomposition applies to the observation-level `update_trace()` method.

**v3:**

```python
span.update_trace(
    name="trace-name",
    user_id="user-123",
    session_id="session-abc",
    input={"query": "hello"},
    output={"result": "world"},
    public=True,
)
```

**v4:**

```python
from langfuse import get_client, propagate_attributes

langfuse = get_client()

with langfuse.start_as_current_observation(as_type="span", name="my-operation") as span:
    with propagate_attributes(trace_name="trace-name", user_id="user-123", session_id="session-abc"):
        result = call_llm("hello")

    span.set_trace_io(input={"query": "hello"}, output={"result": result})  # deprecated
    span.set_trace_as_public()
```

  For integrations ([LangChain](/integrations/frameworks/langchain),
  [OpenAI](/integrations/model-providers/openai-py)), passed-in trace attributes
  now propagate to children only — they do not bubble up to the trace.

### Public API namespace remapping (`api.*`)

In v4, high-performance Public API resources are now the defaults. The v2
aliases were removed.

| v3 / transitional name                  | v4 name                               |
| --------------------------------------- | ------------------------------------- |
| `langfuse.api.observations_v_2`         | `langfuse.api.observations`           |
| `langfuse.api.score_v_2`                | `langfuse.api.scores`                 |
| `langfuse.api.metrics_v_2`              | `langfuse.api.metrics`                |
| `langfuse.api.observations` (legacy v1) | `langfuse.api.legacy.observations_v1` |
| `langfuse.api.score` (legacy v1)        | `langfuse.api.legacy.score_v1`        |
| `langfuse.api.metrics` (legacy v1)      | `langfuse.api.legacy.metrics_v1`      |

If your Python SDK v4 client must temporarily query a self-hosted Langfuse v3
server, use the corresponding `langfuse.api.legacy.<resource>_v1` namespace.

  The new default `langfuse.api.observations` and `langfuse.api.metrics`
  methods point to the Observations v2 and Metrics v2 endpoints, which require
  Langfuse v4 (Langfuse Cloud, or a self-hosted server upgraded to v4). On
  self-hosted Langfuse v3, use `langfuse.api.legacy.observations_v1` and
  `langfuse.api.legacy.metrics_v1` instead. See the [self-hosted
  compatibility matrix](/self-hosting/upgrade/versioning#sdk-server).

  **Public API endpoint deprecations are separate from the v4 breaking
  changes.** Some API methods remain callable in Python v4 but call server
  endpoints that are deprecated for Langfuse v4. After upgrading, use the
  [Python SDK method mappings in the deprecated API migration
  guide](/faq/all/deprecated-api-migration#sdk-method-quick-reference) to audit
  methods such as `langfuse.api.trace.list()`,
  `langfuse.api.sessions.list()`, and `langfuse.api.scores.get_many()`.

### `start_span()` / `start_generation()` → `start_observation()`

Observations are the primary concept in the new model. The unified `start_observation()` API with `as_type` parameter replaces the separate methods.

| v3                                                              | v4                                                                                     |
| --------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `langfuse.start_span(name="x")`                                 | `langfuse.start_observation(name="x")`                                                 |
| `langfuse.start_as_current_span(name="x")`                      | `langfuse.start_as_current_observation(name="x")`                                      |
| `langfuse.start_generation(name="x", model="gpt-4")`            | `langfuse.start_observation(name="x", as_type="generation", model="gpt-4")`            |
| `langfuse.start_as_current_generation(name="x", model="gpt-4")` | `langfuse.start_as_current_observation(name="x", as_type="generation", model="gpt-4")` |
| `span.start_span(name="x")`                                     | `span.start_observation(name="x")`                                                     |
| `span.start_as_current_span(name="x")`                          | `span.start_as_current_observation(name="x")`                                          |
| `span.start_generation(name="x")`                               | `span.start_observation(name="x", as_type="generation")`                               |
| `span.start_as_current_generation(name="x")`                    | `span.start_as_current_observation(name="x", as_type="generation")`                    |

### `DatasetItemClient.run()` removed → use [Experiment SDK](/docs/evaluation/experiments/experiments-via-sdk)

The [Experiment SDK](/docs/evaluation/experiments/experiments-via-sdk) (`dataset.run_experiment()`) handles propagation of experiment attributes (run metadata, dataset item linking) under the hood.

**v3:**

```python
for item in dataset.items:
    with item.run(run_name="my-run", run_metadata={...}) as span:
        result = my_llm(item.input)
        span.update(output=result)
```

**v4:**

```python
from langfuse import get_client

dataset = get_client().get_dataset("my-dataset")

def my_task(*, item, **kwargs):
    return my_llm(item.input)

dataset.run_experiment(name="my-run", task=my_task)
```

The `DatasetItem` objects still have the same data attributes (`id`, `input`, `expected_output`, `metadata`, etc.) but the `run()` method is removed.

### [LangChain](/integrations/frameworks/langchain) `CallbackHandler`: `update_trace` parameter removed

The handler now uses `propagate_attributes()` internally. The `update_trace` parameter no longer exists — passing it raises a `TypeError`.

**v3:**

```python
from langfuse.langchain import CallbackHandler

handler = CallbackHandler(update_trace=True, trace_context={...})
```

**v4:**

```python
handler = CallbackHandler(trace_context={...})
```

  You can still set trace attributes (`user_id`, `session_id`, `tags`, etc.) by
  wrapping your LangChain call in an enclosing span with
  `propagate_attributes()`. See the [LangChain integration
  example](/docs/observability/sdk/upgrade-path/python-v2-to-v3#langchain-integration) in the v2
  → v3 migration guide or the [custom trace
  properties](/docs/observability/sdk/instrumentation#add-attributes)
  documentation.

### Removed types

The following types have been removed from `langfuse.types`:

| Removed Type                             | Description                                                                                        |
| ---------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `TraceMetadata`                          | TypedDict with `name`, `user_id`, `session_id`, `version`, `release`, `metadata`, `tags`, `public` |
| `ObservationParams`                      | TypedDict extending `TraceMetadata` with observation fields                                        |
| `MapValue`, `ModelUsage`, `PromptClient` | No longer re-exported from `langfuse.types`, import from `langfuse.model` instead                  |

### Pydantic v1 support dropped

The SDK now requires Pydantic v2. If your application still uses Pydantic v1, you must use the `pydantic.v1` [compatibility shim](https://docs.pydantic.dev/latest/migration/#continue-using-pydantic-v1-features).

### Validation changes

- propagated `metadata`: now `dict[str, str]` with values limited to 200 characters (was `Any`). Non-string values are coerced to strings. Values exceeding the limit are dropped with a warning.
- `user_id`, `session_id`: validated as strings with a maximum length of 200 characters. Values exceeding the limit are dropped with a warning.

## Migration Checklist

1. Audit traces/dashboards that depended on non-LLM OpenTelemetry spans: these may stop appearing with the v4 default filter
2. If needed, set `should_export_span=lambda span: True` to preserve pre-v4 "export all spans" behavior
3. If you still use `blocked_instrumentation_scopes`, migrate to `should_export_span` composition before the deprecation is removed
4. Search for `update_current_trace` → split into `propagate_attributes()` + `set_current_trace_io()` (only when relying on legacy trace-level LLM-as-a-judge configurations) + `set_current_trace_as_public()`
5. Search for `.update_trace(` → same split on observation objects
6. Search for `start_span` / `start_generation` → replace with `start_observation`
7. Search for `item.run(` → replace with `dataset.run_experiment()`
8. Search for `CallbackHandler(update_trace=` → remove parameter
9. Verify metadata values are `dict[str, str]` with values ≤200 chars
10. Upgrade Pydantic to v2 if still on v1
11. Search for `api.observations_v_2` / `api.score_v_2` / `api.metrics_v_2` → replace with `api.observations` / `api.scores` / `api.metrics`
12. Search for legacy v1 usage on `api.observations` / `api.score` / `api.metrics` → move to `api.legacy.observations_v1` / `api.legacy.score_v1` / `api.legacy.metrics_v1`
13. If you self-host Langfuse v3, use `api.legacy.observations_v1` and `api.legacy.metrics_v1`; the default `api.observations` / `api.metrics` require Langfuse v4 (see the [self-hosted compatibility matrix](/self-hosting/upgrade/versioning#sdk-server))
14. Remove any remaining references to `*_v_2` aliases (they were removed in v4)

<!-- agent-instructions -->

---

## Agent Instructions

This page is part of the [Langfuse](https://langfuse.com) documentation, published as plain Markdown for AI agents. Every page is available as Markdown by appending `.md` to its URL, or by sending an `Accept: text/markdown` header. This page: `https://langfuse.com/docs/observability/sdk/upgrade-path/python-v3-to-v4.md`.

### Querying these docs

If the answer is not on this page, query the documentation instead of guessing:

- **Semantic search** across all Langfuse docs, returning an answer with the relevant pages and excerpts. Ask a specific, self-contained question:

  ```bash
  curl -sG "https://langfuse.com/api/search-docs" --data-urlencode "query=How do I trace a LangGraph agent?"
  ```

- **Index of every page**: <https://langfuse.com/llms.txt>, with per-section indexes [llms-docs.txt](https://langfuse.com/llms-docs.txt), [llms-integrations.txt](https://langfuse.com/llms-integrations.txt), and [llms-self-hosting.txt](https://langfuse.com/llms-self-hosting.txt).

### Before writing Langfuse code

- **Install the [Langfuse Agent Skill](https://langfuse.com/docs/api-and-data-platform/features/agent-skill).** It encodes Langfuse's own best practices for instrumentation, prompt management, and evaluation, and materially improves results.
- **Read [What does a good trace look like?](https://langfuse.com/docs/observability/best-practices.md)** before instrumenting an application.
- **Verify endpoints, parameters, and response fields** against the [API reference](https://api.reference.langfuse.com) instead of inferring them from code examples.
- **Use the [Langfuse CLI](https://langfuse.com/docs/api-and-data-platform/features/cli)** (`npx langfuse-cli api <resource> <action>`) to read or write traces, prompts, datasets, and scores from the terminal.

Found an error in these docs? Please open an issue at <https://github.com/langfuse/langfuse-docs/issues>.
