Skip to main content
Pydantic Evals is the evaluation library from the Pydantic team. It models test suites as a Dataset of Case objects and scores them with deterministic evaluators or with LLMJudge, an LLM-as-judge evaluator driven by a plain-language rubric. This guide shows both ways to wire Pydantic Evals into Arize AX: Flow 1 grades existing Arize AX traces with an LLMJudge and writes the scores back via client.spans.update_evaluations(...); Flow 2 uploads a small dataset, runs an Arize AX experiment with the same rubric wrapped as an experiment evaluator, and surfaces the scores in Datasets+Experiments. Both flows share the same setup. Run the code blocks below in order inside a single Python session — each block builds on imports and variables from earlier ones.

Prerequisites

  • Python 3.11+
  • An ARIZE_SPACE_ID and ARIZE_API_KEY from your Arize AX space settings
  • An OPENAI_API_KEY from OpenAI Platform (used as both the model under trace and the LLMJudge judge model)

Launch Arize AX

If you don’t already have an Arize AX account, sign up at arize.com and grab your ARIZE_SPACE_ID and ARIZE_API_KEY from Settings → Space Settings.

Install

Configure credentials

Define evaluators

The shared setup: a Pydantic Evals LLMJudge that checks each answer against a reference answer, the canonical 2-row hallucination dataset that both flows score, and an Arize SDK client. include_input=True and include_expected_output=True send the question and the reference answer to the judge along with the output. The assertion config names the result factuality and keeps the judge’s reason, so the pass/fail verdict comes back with an explanation.

Flow 1 — Evaluate existing traces

Source the spans

Instrument OpenAI with OpenInference, make two calls (each forced to echo a known answer so the trace contains predictable text), then pull the resulting spans back from Arize AX.

Run the evaluators

Turn each exported span into a Pydantic Evals Case, with the span’s question and ID as the inputs and the reference answer as the expected output. The task looks up the span’s recorded output by ID, so the judge grades exactly what the traced model returned. evaluate_sync(...) runs the LLMJudge over every case and returns a report whose assertions hold the pass/fail verdicts.

Log evaluations to Arize AX

update_evaluations(...) requires a context.span_id column (which export_to_df already provides) plus the reserved eval.<name>.{score,label,explanation} columns. Each LLMJudge verdict becomes one row in this DataFrame.

Expected output

Verify in Arize AX

Open the project named pydantic-evals-tracing-example-<timestamp> (the value printed above) in your Arize AX space. Each ChatCompletion span now carries a factuality annotation column showing the score, the factual / hallucinated label, and the judge’s explanation.

Flow 2 — Run an experiment

Create a dataset

The dataset is the same two rows. The space= / examples= kwarg names match the v8 SDK exactly (note: not space_id= and not dataframe=).

Define the task

The task function receives the dataset row and returns whatever the experiment should grade. The parameter name must be one of input, output, metadata, or dataset_row — a single-arg task with an unrecognized name is bound to dataset_row by default. A real workflow would call an LLM here; this passthrough keeps the example deterministic.

Wrap the evaluators

Experiment evaluators run inside an asyncio loop, so the wrapper is an async def that awaits judge_input_output_expected(...) — the same judge call LLMJudge makes, with the same rubric and model. Calling Dataset.evaluate_sync(...) here instead would fail, because it starts its own event loop. Return an EvaluationResult with score and label and explanation populated: leaving any of those reserved fields as None triggers unsupported cast from null to <type>: reserved column cannot be coerced to canonical type at upload time.

Run the experiment

Expected output

Verify in Arize AX

Open the Datasets + Experiments tab in Arize AX. The dataset pydantic-evals-experiment-example-ds-<timestamp> and the experiment pydantic-evals-experiment-example-<timestamp> (names printed above) appear with one run per dataset row, each carrying the factuality score and label columns.

Troubleshooting

  • This event loop is already running or asyncio.run() cannot be called from a running event loop. Dataset.evaluate_sync(...) starts its own event loop, so it fails inside a Jupyter notebook or an experiment evaluator. In a notebook, await dataset.evaluate(task) instead. In an experiment evaluator, await judge_input_output_expected(...) directly as Flow 2 does.
  • KeyError: 'factuality' when reading case.assertions. The LLMJudge failed for that case, so Pydantic Evals recorded it in case.evaluator_failures instead of case.assertions. Print report.cases[0].evaluator_failures to see the error — usually a missing OPENAI_API_KEY or an unknown judge model.
  • Judge calls appear as extra spans in the Flow 1 project. The judge talks to OpenAI through the same openai client the instrumentor patches. Call instrumentor.uninstrument() before evaluating, as shown in Source the spans.
  • column "eval.factuality.label": unsupported cast from null to string: reserved column cannot be coerced to canonical type. Your experiment evaluator returned a bare bool or number instead of a fully-populated EvaluationResult(score=..., label=..., explanation=...). Populate all three fields.
  • Using a different judge provider. JUDGE_MODEL takes any Pydantic AI model string, such as anthropic:<model> or google-gla:<model>. Install that provider’s extra (for example pip install 'pydantic-ai-slim[anthropic]') and export its API key.
  • Spans never appear after 60s. Span flush + ingest typically takes 5–15s. If the loop times out, check that ARIZE_SPACE_ID + ARIZE_API_KEY are right and that you’re connecting to the correct region’s OTLP endpoint (otlp.arize.com for US, otlp.eu.arize.com for EU).
  • Experiment re-runs collide. Both names embed TIMESTAMP = int(time.time()) so a single re-run produces unique names. If you re-execute the same combined.py quickly, regenerate TIMESTAMP first or call arize.experiments.delete(...) / arize.datasets.delete(...) on the prior run’s names.

Resources

Pydantic Evals Documentation

Pydantic Evals on GitHub

Logging evaluations to Arize AX

Pydantic AI tracing in Arize AX