Python v3 → v4
The Python SDK v4 introduces the observation-centric 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
from langfuse import Langfuse
langfuse = Langfuse(should_export_span=lambda span: True)Compose custom filters with default behavior
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:
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 setLANGFUSE_DEBUG="True". - See SDK advanced features and OpenTelemetry troubleshooting for unwanted spans.
update_current_trace() decomposed into 3 methods
In the new model, correlating attributes (user_id, session_id, metadata, tags) 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:
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):
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"],
):
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=...) | Removed — use LANGFUSE_TRACING_ENVIRONMENT env var |
set_current_trace_io() is deprecated and exists only for backward
compatibility with trace-level
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:
span.update_trace(
name="trace-name",
user_id="user-123",
session_id="session-abc",
input={"query": "hello"},
output={"result": "world"},
public=True,
)v4:
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()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
The Experiment SDK (dataset.run_experiment()) handles propagation of experiment attributes (run metadata, dataset item linking) under the hood.
v3:
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:
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 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:
from langfuse.langchain import CallbackHandler
handler = CallbackHandler(update_trace=True, trace_context={...})v4:
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 in the v2
→ v3 migration guide or the custom trace
properties
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.
Validation changes
- propagated
metadata: nowdict[str, str]with values limited to 200 characters (wasAny). 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
- Audit traces/dashboards that depended on non-LLM OpenTelemetry spans: these may stop appearing with the v4 default filter
- If needed, set
should_export_span=lambda span: Trueto preserve pre-v4 “export all spans” behavior - If you still use
blocked_instrumentation_scopes, migrate toshould_export_spancomposition before the deprecation is removed - Search for
update_current_trace→ split intopropagate_attributes()+set_current_trace_io()(only when relying on legacy trace-level LLM-as-a-judge configurations) +set_current_trace_as_public() - Search for
.update_trace(→ same split on observation objects - Search for
start_span/start_generation→ replace withstart_observation - Search for
item.run(→ replace withdataset.run_experiment() - Search for
CallbackHandler(update_trace=→ remove parameter - Verify metadata values are
dict[str, str]with values ≤200 chars - Upgrade Pydantic to v2 if still on v1