Langfuse v4: up to 165× faster · Read more
DocsPython v4 → v5

Python v4 → v5

The Python SDK v5 targets Langfuse v4 only. It removes the Public API methods that Langfuse v4 deprecated, records experiments through OpenTelemetry instead of the dataset-run-items endpoint, and sends gzip-compressed span batches with the v4 ingestion header by default.

Most applications that already follow the observations-first model only need to check a few call sites. Start with the migration checklist and use the sections below for details.

Python SDK v5 requires Langfuse Cloud or self-hosted Langfuse v4.XX.0 or later. On older v4 servers such as 4.55.x, plain string metadata values like "123" or "true" are stored as numbers or booleans. If you self-host Langfuse v3, stay on Python SDK v4. See the self-hosted compatibility matrix and Versions & Compatibility.

host and the LANGFUSE_HOST environment variable keep working as before. You don't need to change them.

Breaking Changes

Python 3.11 and OpenAI SDK 1.x required

v5 requires Python >=3.11. On Python 3.10, pip install langfuse resolves to the latest v4 release.

The OpenAI integration (from langfuse.openai import openai) requires openai>=1.0. Support for pre-1.0 OpenAI SDKs was removed.

LangChain v1 required

The LangChain CallbackHandler now imports only from langchain_core and requires langchain-core>=1. You no longer need the langchain meta-package to use the handler.

pip install --upgrade "langfuse>=5" "langchain-core>=1"

The import path and usage are unchanged:

from langfuse.langchain import CallbackHandler

handler = CallbackHandler()
chain.invoke({"input": "..."}, config={"callbacks": [handler]})

Spans are gzip-compressed by default

The default OTLP span exporter now sends gzip-compressed batches. Compression is resolved in this order:

  1. otel_compression argument on Langfuse(...)
  2. LANGFUSE_OTEL_COMPRESSION
  3. OTEL_EXPORTER_OTLP_TRACES_COMPRESSION
  4. OTEL_EXPORTER_OTLP_COMPRESSION
  5. gzip

Supported values are gzip and none (case-insensitive). An invalid value falls through to the next setting. If a proxy between your application and Langfuse can't handle compressed requests, opt out:

from langfuse import Langfuse

langfuse = Langfuse(otel_compression="none")

or set LANGFUSE_OTEL_COMPRESSION=none. A custom span_exporter is not changed; configure compression on that exporter directly.

v4 ingestion header sent by default

The default span exporter sends x-langfuse-ingestion-version: 4, which enables real-time processing of spans on Langfuse v4. To opt out, override the header:

from langfuse import Langfuse

langfuse = Langfuse(additional_headers={"x-langfuse-ingestion-version": "3"})

A custom span_exporter does not receive this header automatically. Add it to your exporter if you send spans to Langfuse v4.

blocked_instrumentation_scopes removed

The parameter was deprecated in v4 and now raises a TypeError. Use should_export_span and compose it with the default filter.

v4:

from langfuse import Langfuse

langfuse = Langfuse(blocked_instrumentation_scopes=["sqlite", "requests"])

v5:

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
        )
    )
)

The default filter's GenAI check now requires the gen_ai. prefix (with the dot). Attributes such as gen_ai_custom no longer cause a span to be exported.

Trace input/output setters removed

set_current_trace_io(), span.set_trace_io(), and the trace-level input/output attributes were deprecated in v4 and are removed. Trace input and output now come from the root observation. Set them on the root span instead.

v4:

from langfuse import get_client

langfuse = get_client()

with langfuse.start_as_current_observation(as_type="span", name="request") as span:
    result = call_llm(query)
    langfuse.set_current_trace_io(input=query, output=result)

v5:

from langfuse import get_client

langfuse = get_client()

with langfuse.start_as_current_observation(as_type="span", name="request") as span:
    result = call_llm(query)
    span.update(input=query, output=result)
    # or, inside the active root span:
    # langfuse.update_current_span(input=query, output=result)

set_current_trace_as_public() and span.set_trace_as_public() are unchanged. If you still use trace-level LLM-as-a-judge evaluators, migrate them to observation-level evaluators.

Propagated metadata is serialized as JSON

propagate_attributes(metadata=...) now serializes non-string values as compact JSON instead of with str(). Strings are unchanged. Self-hosted servers need v4.XX.0 or later to keep string values that look like numbers or booleans as strings.

Valuev4v5
True"True""true"
None"None""null"
["a", 1]"['a', 1]"'["a",1]'
{"k": "v"}"{'k': 'v'}"'{"k":"v"}'
1.0"1.0""1.0"
datetime(...)str(datetime)ISO 8601 string
("a", "b"), setPython reprJSON array
float("nan")"nan"Dropped with a warning

Non-ASCII characters are kept as-is and large integers keep all digits. The 200-character limit applies to the serialized value. If you filter or group by metadata values that were previously Python reprs (for example "True"), update those filters, dashboards, or downstream consumers.

Dataset run helpers removed

The deprecated dataset run helpers are removed. Use the experiments API instead. "Dataset run" and "experiment" refer to the same concept.

RemovedReplacement
langfuse.get_dataset_runs(dataset_name=...)langfuse.api.experiments.list(dataset_id=..., from_start_time=...)
langfuse.get_dataset_run(dataset_name=..., run_name=...)langfuse.api.experiments.list(...) and .list_items(...)
langfuse.delete_dataset_run(dataset_name=..., run_name=...)List the experiment items, then langfuse.api.trace.delete_multiple(...)

v4:

runs = langfuse.get_dataset_runs(dataset_name="qa-set")
run = langfuse.get_dataset_run(dataset_name="qa-set", run_name="baseline")
langfuse.delete_dataset_run(dataset_name="qa-set", run_name="baseline")

v5:

from datetime import datetime, timedelta, timezone

from langfuse import get_client

langfuse = get_client()
dataset = langfuse.get_dataset("qa-set")
since = datetime.now(timezone.utc) - timedelta(days=30)

# List experiments (dataset runs) for a dataset
experiments = langfuse.api.experiments.list(dataset_id=dataset.id, from_start_time=since)

# List the items of one experiment and follow cursor pagination
trace_ids = set()
cursor = None
while True:
    page = langfuse.api.experiments.list_items(
        dataset_id=dataset.id,
        experiment_name="baseline",
        from_start_time=since,
        cursor=cursor,
    )
    trace_ids.update(item.trace_id for item in page.data)
    cursor = page.meta.cursor
    if not cursor:
        break

# Delete the experiment's traces (also deletes their observations and scores)
ids = list(trace_ids)
for i in range(0, len(ids), 1000):
    langfuse.api.trace.delete_multiple(trace_ids=ids[i : i + 1000])

There is no experiment-delete API. Deleting the traces also deletes their observations and scores. See Dataset runs → Experiments for details.

Experiment runner records experiments through OpenTelemetry

langfuse.run_experiment() and dataset.run_experiment() no longer call POST /api/public/dataset-run-items. Experiment and item attributes are sent on the experiment spans instead. Your experiment code does not need to change, but a few results do:

  • Experiment IDs are deterministic. The experiment ID is derived from the project, dataset, and run name, so running again with the same run_name on the same dataset adds to the same experiment. Experiments on local data get a random ID.
  • New result fields. result.experiment_id and result.experiment_url are the primary fields. dataset_run_id and dataset_run_url (and ExperimentItemResult.dataset_run_id) still work but emit a DeprecationWarning.
  • Local runs have an ID too. dataset_run_id is now set for local-data runs, so if result.dataset_run_id: no longer distinguishes dataset runs from local runs.
  • New URL. experiment_url points to /project/{projectId}/experiments/results?baseline={experimentId}.
  • Run evaluations are always stored, including for local-data runs. They are skipped only when every item trace was sampled out.
  • Dataset versions are recorded on items as langfuse.experiment.item.version.

v4:

result = dataset.run_experiment(name="baseline", task=my_task, evaluators=[accuracy])
print(result.dataset_run_url)

v5:

result = dataset.run_experiment(name="baseline", task=my_task, evaluators=[accuracy])
print(result.experiment_url)

Batched evaluation reads observations only

langfuse.run_batched_evaluation() now reads from the v2 observations API. It evaluates observations only.

  • scope is removed. To evaluate whole traces, filter for the observations that represent them, for example by name or type.
  • fetch_trace_fields is renamed to fields (default "core,basic,io,metadata"). Available field groups: core, basic, time, io, metadata, model, usage, prompt, metrics, trace_context.
  • The mapper receives item=ObservationV2. input and output are raw strings.
  • filter must be a v2 filter JSON array.
  • _additional_trace_tags is removed.
  • Resume tokens from v4 can't be reused, and resuming with a different filter raises a ValueError.

v4:

def trace_mapper(*, item):
    return EvaluatorInputs(input=item.input, output=item.output, expected_output=None)

langfuse.run_batched_evaluation(
    scope="traces",
    mapper=trace_mapper,
    filter='{"tags": ["production"]}',
    fetch_trace_fields="core,io",
    evaluators=[accuracy],
)

v5:

from langfuse import EvaluatorInputs, get_client

langfuse = get_client()

def observation_mapper(*, item, **kwargs):
    return EvaluatorInputs(
        input=item.input,
        output=item.output,
        expected_output=None,
        metadata={"trace_id": item.trace_id},
    )

langfuse.run_batched_evaluation(
    mapper=observation_mapper,
    filter='[{"type": "string", "column": "type", "operator": "=", "value": "GENERATION"}]',
    fields="core,basic,io,model",
    evaluators=[accuracy],
)

Removed Public API methods and types

The generated client under langfuse.api no longer includes the endpoints that Langfuse v4 deprecated. For full replacement guidance, see the SDK method quick reference.

RemovedReplacement
api.trace.get(...), api.trace.list(...)api.observations.get_many(trace_id=..., ...)
api.sessions.*api.observations.get_many(session_id=..., ...)
api.legacy.observations_v1.*api.observations.get_many(...)
api.legacy.metrics_v1.metrics(...)api.metrics.metrics(...)
api.scores.get_many(...), api.scores.get_by_id(...)api.scores_v3.get_many_v3(...)
api.dataset_run_items.*api.experiments.list_items(...) or the experiment runner
api.datasets.get_run(...), .get_runs(...), .delete_run(...)Dataset run helpers removed

api.trace keeps delete and delete_multiple. api.datasets keeps list, get, and create.

Removed types include Trace, TraceWithDetails, TraceWithFullDetails, Traces, Observation, ObservationsView, ObservationsViews, Session, SessionWithTraces, PaginatedSessions, DatasetRun, DatasetRunItem, DatasetRunWithItems, PaginatedDatasetRuns, PaginatedDatasetRunItems, CreateDatasetRunItemRequest, DeleteDatasetRunResponse, the ScoreV1 family, GetScoresResponse*, the v1 MetricsResponse, and the sdk-log ingestion event. langfuse.model no longer re-exports DatasetRun, Observation, or TraceWithFullDetails.

v4:

trace = langfuse.api.trace.get("trace-id")

v5:

observations = langfuse.api.observations.get_many(trace_id="trace-id", fields="core,basic,io")

api.scores.create takes a request object

langfuse.api.scores.create() now accepts a single CreateScoreRequest or a list of them, and returns CreateScoresResponse (a union of the single-score and batch responses).

v4:

langfuse.api.scores.create(name="accuracy", value=0.9, trace_id="trace-id")

v5:

from langfuse.api import CreateScoreRequest

langfuse.api.scores.create(
    request=CreateScoreRequest(name="accuracy", value=0.9, trace_id="trace-id")
)

langfuse.api.legacy.score_v1.create(name=..., value=..., ...) keeps the v4 keyword signature and returns a single CreateScoreResponse. It's a deprecated compatibility alias for api.scores.create.

The SDK's langfuse.create_score(...), span.score(...), and span.score_trace(...) are unchanged.

Migration Checklist

  1. Confirm you're on Langfuse Cloud or self-hosted Langfuse v4.XX.0 or later. Stay on Python SDK v4 for Langfuse v3.
  2. Upgrade to Python 3.11+, openai>=1.0, and langchain-core>=1 if you use those integrations.
  3. If a proxy rejects compressed requests, set otel_compression="none" or LANGFUSE_OTEL_COMPRESSION=none.
  4. Search for blocked_instrumentation_scopes → replace with should_export_span composed with is_default_export_span.
  5. Search for set_current_trace_io / set_trace_io → set input/output on the root observation.
  6. Check filters, dashboards, and consumers that read propagated metadata values written by str().
  7. Search for get_dataset_run, get_dataset_runs, delete_dataset_run → use api.experiments.list / list_items and api.trace.delete_multiple.
  8. Search for dataset_run_id / dataset_run_url → use experiment_id / experiment_url.
  9. Search for run_batched_evaluation → remove scope, rename fetch_trace_fields to fields, update the mapper and filter.
  10. Search for api.trace.get, api.trace.list, api.sessions, api.legacy.observations_v1, api.legacy.metrics_v1, api.scores.get_many, api.scores.get_by_id, api.dataset_run_items, api.datasets.get_run → see the SDK method quick reference.
  11. Search for api.scores.create( → pass request=CreateScoreRequest(...).

Was this page helpful?

Last updated on