Upgrade Guide
In September 2025, Pydantic AI reached V1 and committed to API stability: no changes that break your code until V2. V2 is now available as a beta pre-release, collecting the breaking and behavior changes that stability guarantee didn't allow. This guide is the canonical place to learn what's in V2, how to install it, and how to upgrade; for the guarantees behind these version numbers, see the Version Policy.
Breaking Changes
Here's a filtered list of the breaking changes for each version to help you upgrade Pydantic AI.
v2.0.0b1 (2026-05-20)
The first V2 beta, forked from v1.100.0, which deprecates most of what V2 removes. V2 leans into a harness-first design with capabilities as a core primitive: a single, composable unit that bundles an agent's tools, hooks, instructions, and model settings, reaching every layer of the agent through one concept. Many of V2's changes move configuration that used to be spread across Agent arguments onto that primitive, alongside the behavior changes that V1's stability guarantee didn't allow. Pydantic AI stays a small core: some capabilities ship with it, more come from the first-party Pydantic AI Harness, and others are third-party or your own.
To install the beta, pin the exact pre-release version. Find the current beta on PyPI or the GitHub releases page, then (replacing bN with that version):
pip install "pydantic-ai==2.0.0bN"
uv add "pydantic-ai==2.0.0bN"
The V2 API and behaviors aren't yet covered by our stability guarantee — we don't expect major changes but may still adjust in response to feedback before the stable V2.0 release. Please try it and report issues, or reach out in the #pydantic-ai channel on Slack.
The breaking changes below are split into two groups:
- Changes not covered by deprecation warnings — removals and behavior changes that couldn't be announced via a V1 deprecation warning. Review these even if you're already on the latest V1 with no warnings.
- Changes covered by deprecation warnings — if you upgraded to the latest V1 and resolved every deprecation warning, you've already made these. They're listed with full before → after for reference.
Recommended upgrade path. To make the jump as smooth as possible:
- Upgrade to the latest V1 release. Most of what V2 removes is deprecated as of v1.100.0 (the release this beta is forked from), so any V1 at or above that version surfaces those warnings.
- Resolve every deprecation warning. The changes covered by deprecation warnings were announced in V1 via warnings that name the new API and, where possible, include a migration snippet. Run your test suite (or app) with warnings visible and address each one — by hand or by pointing a coding agent at them — to migrate across the bulk of V2 ahead of time.
- Upgrade to V2 and make the changes not covered by deprecation warnings — primarily default-behavior changes and a handful of removals with no V1 deprecation.
You can also upgrade straight to V2 and work through the list below directly — it's organized so a coding agent can apply the code changes mechanically. Resolving deprecation warnings on the latest V1 first is still the smoother path, since it spreads the work out and leaves you only the behavior changes to reason about consciously at the end.
Message history serialized with V1 (via ModelMessagesTypeAdapter) continues to deserialize in V2.
Changes not covered by deprecation warnings
These removals and behavior changes could not be announced via a V1 deprecation warning, so review them even if you've resolved every deprecation warning on the latest V1.
Code changes:
- Generic type parameter defaults changed from
Nonetoobject: an un-parameterizedAgent(...)now infersAgent[object, str]instead ofAgent[None, str], and thepydantic_graphStateT/RunEndT/DepsTdefaults changed to match. Update explicitAgent[None, ...],RunContext[None], andTool[None]annotations that don't actually requireNonedependencies to useobject. This is a type-checking-only change; runtime behavior is unchanged. See #5307. - The
pydantic_graph.persistencepackage and thepydantic_graph.mermaidmodule are removed, with no V2 equivalent for graph state persistence or standalone Mermaid generation (render diagrams withGraph.render()). The move of theGraphBuilderAPI out ofpydantic_graph.betato the top-levelpydantic_graphwas deprecation-announced; see below. See #5470. ModelProfileand its subclasses are nowTypedDicts instead of dataclasses. Passingprofile=OpenAIModelProfile(field=value)into a model still works unchanged; the migration only matters if you read or mutate profile fields, or call.update()/.from_profile(). SeeModelProfileis now aTypedDictbelow. (#5481)
Default behavior changes — same API, different runtime behavior (roughly ordered by how many users they affect):
- A bare
uv add pydantic-ai/pip install pydantic-ainow installs a slimmer set of extras (frontier providers plus minimal integrations); providers likebedrock,groq, andmistralare no longer included by default, so you'll need to add the extras you use. See Slimmer default extras below. (#5467) - The default
end_strategychanged from'early'to'graceful': when a model calls function tools in the same response as a successful output tool, those function tools now run (and their side effects happen) instead of being skipped, and tool calls run in the order the model emitted them. See Parallel tool-call execution order below. (#5339) - The default instrumentation format is now version 5, and agent run spans report token usage under
gen_ai.aggregated_usage.*. See Instrumentation defaults below. (#5523) capture_run_messages()now also captures the partialModelRequest/ModelResponsefrom an interrupted run, marked withstate='interrupted'(a newModelRequest.statefield is added). Code that asserts on exact captured-message counts on error paths may need updating. See #5364.- Output tool calls and returns now emit dedicated
OutputToolCallEvent/OutputToolResultEventinstead ofFunctionToolCallEvent/FunctionToolResultEvent. Separately, native tool calls and returns no longer emit dedicated events at all — theBuiltinToolCallEvent/BuiltinToolResultEventclasses are removed and they surface only via the standardPartStartEvent/PartDeltaEvent. See #5332 and #5476.
ModelProfile is now a TypedDict
See the Model Profile guide for an overview of what a model profile is and how to configure one.
ModelProfile and all its subclasses (OpenAIModelProfile, AnthropicModelProfile, GoogleModelProfile, BedrockModelProfile, etc.) are now TypedDict(total=False) instead of @dataclass. This unifies the mental model with ModelSettings (also a TypedDict) and enables direct dict-spread for cross-class merging.
ModelProfile.update() and ModelProfile.from_profile() are removed; use the module-level merge_profile (later argument wins per key).
Migration recipes:
| v1 (dataclass) | v2 (TypedDict) |
|---|---|
OpenAIModelProfile(field=value) |
Same syntax; returns a partial dict instead of a fully-defaulted instance. |
profile.field (attribute read) |
profile.get('field', <default>) — non-trivial defaults are exported from pydantic_ai.profiles (e.g. DEFAULT_THINKING_TAGS, DEFAULT_PROMPTED_OUTPUT_TEMPLATE); the fully-merged base is DEFAULT_PROFILE. |
profile.field = value (attribute write) |
profile['field'] = value |
dataclasses.replace(profile, field=value) |
{**profile, 'field': value} or merge_profile(profile, ModelProfile(field=value)) |
profile.update(other) |
merge_profile(profile, other) |
OpenAIModelProfile.from_profile(p) |
Just p — no upcasting needed |
Model(name, profile=full_profile) (full replace) |
Now merges on top of the provider's default profile — usually what you want. For a hard replace use Model(name, profile=lambda _default: full_profile). |
Model(name, profile=fn) where fn: Callable[[str], ModelProfile \| None] |
Removed — the user-passed callable is now Callable[[ModelProfile], ModelProfile], receiving the resolved default and returning the final profile. The (model_name: str) -> ModelProfile \| None shape is still accepted internally by Provider.model_profile. |
isinstance(profile, OpenAIModelProfile) |
Not supported by TypedDict at runtime — raises TypeError. Use isinstance(profile, dict) or check key presence ('openai_chat_supports_web_search' in profile). Pyright still narrows correctly via the TypedDict subclass annotation. |
Model.profile is now the single source of truth for the resolved profile. It is composed by merge_profile in this order (later wins):
DEFAULT_PROFILE— base defaults for every documented key.Provider.model_profile(model_name)— provider/model-specific resolution.- The user's
profile=argument — either a partial dict (merged on top) or aCallable[[ModelProfile], ModelProfile](full control: receives the resolved default, returns the final profile).
Resolved profiles now carry cross-class fields
In v1, ModelProfile.update() silently filtered out fields not declared on the target class. In v2, dict-spread preserves every key.
This means e.g. a Bedrock-hosted Anthropic model's resolved profile now carries the upstream anthropic_* fields alongside the bedrock_* fields, where v1 dropped them. No in-tree model class reads cross-class fields, so behavior is unchanged in the standard providers; but custom model classes that do profile.get('anthropic_supports_adaptive_thinking', False) on a non-Anthropic route will now see the value the upstream Anthropic profile set, where v1 always returned the default.
See the Model Profile guide for how to configure a profile, and PR #5481 for the full ModelProfile redesign.
Parallel tool-call execution runs in emission order
The default end_strategy changed from 'early' to 'graceful'. This only affects responses where a model calls function tools in the same response as an output tool (the call that ends the run). When that output tool succeeds, the function tools requested alongside it now run by default instead of being skipped, so their side effects happen and their results reach the model if the run continues; and a function tool's ModelRetry now suppresses the output result so the model can correct itself on the next round. The case where every output tool fails is unchanged: function tools run and the run continues either way. Most agents don't need any change. If you relied on the run ending the instant an output tool succeeds — skipping any function tools requested in the same response — set end_strategy='early' explicitly.
The sequential=True flag on a tool is now a per-tool barrier rather than a batch-wide serial switch: a sequential tool runs alone, but other tools in the same response still run in parallel around it. The barrier now also applies to output tools via ToolOutput(sequential=True), not just function tools. To run all of a run's tools serially, wrap the run in agent.parallel_tool_call_execution_mode('sequential') or set parallel_tool_calls=False on the model settings.
See Parallel Output Tool Calls for the full behavior of all three strategies, and #5339.
Slimmer default pydantic-ai extras
A bare uv add pydantic-ai / pip install pydantic-ai now installs pydantic-ai-slim[openai,anthropic,google,cli,mcp,evals,web,retries,logfire] — frontier providers plus minimal integrations. Providers and integrations that were previously bundled are no longer installed by default; add the ones you use explicitly, e.g. uv add 'pydantic-ai[bedrock,groq]': bedrock, groq, mistral, cohere, xai, huggingface, temporal, ag-ui, ui, and spec. See the installation guide for the full list of extras.
Some pydantic-ai-slim extras were also removed outright (not just dropped from the default bundle): the outlines-* extras (the Outlines integration is removed), vertexai (Vertex AI is now served by the google extra), fastmcp (the FastMCP back-compat shim is removed), and a2a (A2A now lives in the upstream fasta2a package). See #5467.
Instrumentation defaults to version 5 with aggregated usage attributes
The default instrumentation format is now version 5 (versions 2–4 still work but emit a deprecation warning; version 1 and its event_mode=/logger_provider= arguments are removed). In version 5, deferred tool calls (CallDeferred/ApprovalRequired) are no longer recorded as span errors.
Separately, InstrumentationSettings's use_aggregated_usage_attribute_names now defaults to True: agent run spans report token usage under gen_ai.aggregated_usage.* while model request spans keep gen_ai.usage.*, which avoids double-counting in backends that sum parent and child usage. Dashboards and alerts that read token usage from run spans must be updated, or set use_aggregated_usage_attribute_names=False to keep the V1 attribute names.
See #5523.
Changes covered by deprecation warnings
These changes were announced in the latest V1 releases via deprecation warnings that name the replacement API. If you upgraded to the latest V1 and resolved every warning, you've already made them; they're listed here with full before → after for reference.
Behavior changes that flip silently if the V1 deprecation warning was not addressed — even though these were announced, an unaddressed warning means the behavior changes without raising an error, so confirm you've handled them:
- The bare
openai:model prefix now uses the OpenAI Responses API (OpenAIResponsesModel) instead of the Chat Completions API (OpenAIChatModel). Useopenai-chat:to keep Chat Completions, oropenai-responses:to opt into the new default explicitly. Announced via #5334; flipped in #5469. - Provider-adaptive
WebSearchandWebFetchcapabilities are now native-only and raise on models that don't support them, andMCP(url=...)runs the server locally by default. Restore the V1 fallbacks withWebSearch(local='duckduckgo'),WebFetch(local=True), andMCP(url=..., native=True). Announced via #5331; changed in #5333.
API removals and renames:
pydantic_ai.providers.grok.GrokProviderandpydantic_ai.providers.grok.GrokModelNameare removed; usepydantic_ai.providers.xai.XaiProviderwithpydantic_ai.models.xai.XaiModel(andpydantic_ai.models.xai.XaiModelName). Thegrok:model prefix is removed; usexai:. See #5460.GoogleGLAProvider,GoogleVertexProvider, andGeminiModel(the wholepydantic_ai.models.geminimodule) are removed; usepydantic_ai.providers.google.GoogleProvider(Gemini API) orpydantic_ai.providers.google_cloud.GoogleCloudProvider(Vertex) withpydantic_ai.models.google.GoogleModel. Provider prefixes:google-gla:→google:,google-vertex:→google-cloud:,vertexai:→google-cloud:, andgateway/gemini:/gateway/google-vertex:→gateway/google-cloud:.GoogleProvider(vertexai=, location=, project=, credentials=)→GoogleCloudProvider(...).GoogleModelSettingskeysgoogle_vertex_service_tier/google_service_tier→google_cloud_service_tier. Announced via #5336 and #5543; removed in #5479.OpenAIModel→OpenAIChatModel,OpenAIModelSettings→OpenAIChatModelSettings; theOpenAIChatModel(system_prompt_role=...)kwarg →OpenAIModelProfile(openai_system_prompt_role=...);OpenAICompaction(instructions=...)removed;OpenAIModelProfile.openai_supports_sampling_settings→openai_unsupported_model_settings. See #5468.- Built-in tools are renamed to "native" tools:
pydantic_ai.builtin_tools→pydantic_ai.native_tools;BuiltinToolCallPart/BuiltinToolReturnPart/AgentBuiltinTool→NativeToolCallPart/NativeToolReturnPart/AgentNativeTool;Agent(builtin_tools=[...])→capabilities=[NativeTool(...)];builtin=→native=;OpenAIModelProfile.openai_builtin_tools→openai_native_tools. The serializedpart_kindwire values are unchanged, so message history still deserializes. Announced via #5338; removed in #5396. - MCP:
MCPServerStdio/MCPServerSSE/MCPServerStreamableHTTP/MCPServerHTTP,FastMCPToolset,load_mcp_servers,Agent.run_mcp_servers(), andAgent.set_mcp_sampling_model()are removed; usepydantic_ai.mcp.MCPToolset,pydantic_ai.mcp.load_mcp_toolsets,async with agent:, andMCPToolset(sampling_model=...). Note that the newMCPToolsetdefaults differ (e.g.max_retries,read_timeout,init_timeout,elicitation_handler). Announced via #5325; removed in #5337. Agent(instrument=...),Agent.from_spec(instrument=...),Agent.from_file(instrument=...), andAgentSpec.instrumentare removed; usecapabilities=[Instrumentation(...)]. (TheAgent.instrumentproperty,Agent.instrument_all(), andInstrumentedModelare unchanged.) See #5434.Agent(event_stream_handler=...)→capabilities=[ProcessEventStream(...)];Agent(prepare_tools=...)→capabilities=[PrepareTools(...)]. Theevent_stream_handler=argument onrun()/run_sync()/run_stream()/iter()is unchanged. Announced via #5335; removed in #5475.Agent(history_processors=...)→capabilities=[ProcessHistory(...)]. See #5425.Agent(mcp_servers=[...])→Agent(toolsets=[...]);Agent.sequential_tool_calls()→agent.parallel_tool_call_execution_mode('sequential'). See #5466.Agent.to_a2a()and the bundledfasta2aintegration (and the[a2a]extra) are removed; installfasta2a[pydantic-ai]>=0.6.1and usefrom fasta2a.pydantic_ai import agent_to_a2a. Announced via #5426; removed in #5502.Agent.to_ag_ui(),AGUIApp, and thepydantic_ai.ag_uishim are removed; usepydantic_ai.ui.ag_ui.AGUIAdapter.pydantic_ai.models.cached_async_http_clientis removed; usepydantic_ai.models.create_async_http_client()or your ownhttpx.AsyncClient. Announced via #5345; removed in #5464.pydantic_ai.ext.aci(tool_from_aci,ACIToolset) is removed with no upstream replacement; wrap ACI tools withTool.from_schema. Announced via #5510; removed in #5467.pydantic_ai.output.DeferredToolCalls→DeferredToolRequests;pydantic_ai.toolsets.external.DeferredToolset→ExternalToolset. See #5459.FunctionToolset.tool()now raises if the decorated callable's first parameter is not aRunContext; useFunctionToolset.tool_plain()for context-free tools. See #5462.- Usage/token renames:
request_tokens→input_tokens,response_tokens→output_tokens,Usage→RunUsage,UsageLimits(request_tokens_limit=)→input_tokens_limit=,UsageLimits(response_tokens_limit=)→output_tokens_limit=. Response field renames:ModelResponse.vendor_details→provider_details,vendor_id/provider_request_id→provider_response_id. Removed event-class shimsBuiltinToolCallEvent/BuiltinToolResultEvent, andFunctionToolCallEvent.call_id→.tool_call_id. Message history serialized with the old field names still deserializes via retained validation aliases. See #5476. - Output tool calls now emit dedicated
OutputToolCallEvent/OutputToolResultEventrather thanFunctionToolCallEvent/FunctionToolResultEvent;FunctionToolResultEvent(result=...)/.result→(part=...)/.part. See #5332. StreamedRunResult.stream→stream_output,StreamedRunResult.stream_structured→stream_response,StreamedRunResult.validate_structured_output→validate_response_output; the pluralstream_responses()→ singularstream_response()(which yields a bareModelResponse; read the oldis_lastflag asresponse.state != 'incomplete'). Announced via #5296; removed in #5463.result.usage()→result.usage,result.timestamp()→result.timestamp, andstream.get()→stream.response(method-style accessors become properties). See #5263.StreamedResponse.usage()→StreamedResponse.usage: the model-adapter streaming base class (pydantic_ai.models.StreamedResponse) now exposesusageas a property rather than a method. Relevant if you've subclassedModeland call.usage()on a streamed response. See #5546.pydantic_graph.betaimports move to the top-levelpydantic_graph(e.g.from pydantic_graph import GraphBuilder). Announced via #5306; removed in #5470.- Instrumentation format
version=1and its version-1-onlyInstrumentationSettings(event_mode=...)andInstrumentationSettings(logger_provider=...)arguments are removed (deprecated in V1);version=2/3/4still work but now emit a deprecation warning. The default isversion=5— see Instrumentation defaults above for the default-behavior changes that ship with it. See #5523. - The Outlines integration (
pydantic_ai.models.outlines.OutlinesModel,pydantic_ai.providers.outlines.OutlinesProvider, and theoutlines-*extras) is removed. If you'd like to keep using Outlines with Pydantic AI, please file an issue at dottxt-ai/outlines. See #5444. pydantic_ai.native_tools.UrlContextToolis removed; usepydantic_ai.native_tools.WebFetchToolinstead. See #5458.- Iterating
Agent.run_stream_events()directly is no longer supported; it is now an async context manager only:async with agent.run_stream_events(...) as events: async for event in events: .... See #5440. - The bare (provider-prefix-less) model-name fallback is removed:
Agent('gpt-5')now raises aUserErrorinstead of inferring the provider; pass a provider-prefixed model name likeAgent('openai:gpt-5'). (V1 emitted a deprecation warning for prefix-less legacy model names.) See #5464. - Pydantic Evals:
EvaluationResultandEvaluatorFailureare now keyword-only;Dataset.evaluate()/evaluate_sync()makename/max_concurrency/progress/retry_task/retry_evaluatorskeyword-only;Dataset(name=...)is now required; theEvaluator.nameclassmethod →Evaluator.get_serialization_name(). Announced via #5547; changed in #5548. - Pydantic Evals: custom
Evaluators that advertised a default name or version by setting anevaluation_name/evaluator_versionclass attribute should overrideEvaluator.get_default_evaluation_name()/Evaluator.get_evaluator_version()instead; the attribute fallback is removed. Announced via #5554; removed in #5556.
v1.0.1 (2025-09-05)
The following breaking change was accidentally left out of v1.0.0:
- See #2808 - Remove
Pythonevaluator frompydantic_evalsfor security reasons
v1.0.0 (2025-09-04)
- See #2725 - Drop support for Python 3.9
- See #2738 - Make many dataclasses require keyword arguments
- See #2715 - Remove
casesandaveragesattributes frompydantic_evalsspans - See #2798 - Change
ModelRequest.partsandModelResponse.partstypes fromlisttoSequence - See #2726 - Default
InstrumentationSettingsversion to 2 - See #2717 - Remove errors when passing
AsyncRetryingorRetryingobject toAsyncTenacityTransportorTenacityTransportinstead ofRetryConfig
v0.x.x
Before V1, minor versions were used to introduce breaking changes:
v0.8.0 (2025-08-26)
See #2689 - AgentStreamEvent was expanded to be a union of ModelResponseStreamEvent and HandleResponseEvent, simplifying the event_stream_handler function signature. Existing code accepting AgentStreamEvent | HandleResponseEvent will continue to work.
v0.7.6 (2025-08-26)
The following breaking change was inadvertently released in a patch version rather than a minor version:
See #2670 - TenacityTransport and AsyncTenacityTransport now require the use of pydantic_ai.retries.RetryConfig (which is just a TypedDict containing the kwargs to tenacity.retry) instead of tenacity.Retrying or tenacity.AsyncRetrying.
v0.7.0 (2025-08-12)
See #2458 - pydantic_ai.models.StreamedResponse now yields a FinalResultEvent along with the existing PartStartEvent and PartDeltaEvent. If you're using pydantic_ai.direct.model_request_stream or pydantic_ai.direct.model_request_stream_sync, you may need to update your code to account for this.
See #2458 - pydantic_ai.models.Model.request_stream now receives a run_context argument. If you've implemented a custom Model subclass, you will need to account for this.
See #2458 - pydantic_ai.models.StreamedResponse now requires a model_request_parameters field and constructor argument. If you've implemented a custom Model subclass and implemented request_stream, you will need to account for this.
v0.6.0 (2025-08-06)
This release was meant to clean some old deprecated code, so we can get a step closer to V1.
See #2440 - The next method was removed from the Graph class. Use async with graph.iter(...) as run: run.next() instead.
See #2441 - The result_type, result_tool_name and result_tool_description arguments were removed from the Agent class. Use output_type instead.
See #2441 - The result_retries argument was also removed from the Agent class. Use output_retries instead.
See #2443 - The data property was removed from the FinalResult class. Use output instead.
See #2445 - The get_data and validate_structured_result methods were removed from the
StreamedRunResult class. Use get_output and validate_response_output instead.
See #2446 - The format_as_xml function was moved to the pydantic_ai.format_as_xml module.
Import it via from pydantic_ai import format_as_xml instead.
See #2451 - Removed deprecated Agent.result_validator method, Agent.last_run_messages property, AgentRunResult.data property, and result_tool_return_content parameters from result classes.
v0.5.0 (2025-08-04)
See #2388 - The source field of an EvaluationResult is now of type EvaluatorSpec rather than the actual source Evaluator instance, to help with serialization/deserialization.
See #2163 - The EvaluationReport.print and EvaluationReport.console_table methods now require most arguments be passed by keyword.
v0.4.0 (2025-07-08)
See #1799 - Pydantic Evals EvaluationReport and ReportCase are now generic dataclasses instead of Pydantic models. If you were serializing them using model_dump(), you will now need to use the EvaluationReportAdapter and ReportCaseAdapter type adapters instead.
See #1507 - The ToolDefinition description argument is now optional and the order of positional arguments has changed from name, description, parameters_json_schema, ... to name, parameters_json_schema, description, ... to account for this.
v0.3.0 (2025-06-18)
See #1142 — Adds support for thinking parts.
We now convert the thinking blocks ("<think>..."</think>") in provider specific text parts to
Pydantic AI ThinkingParts. Also, as part of this release, we made the choice to not send back the
ThinkingParts to the provider - the idea is to save costs on behalf of the user. In the future, we
intend to add a setting to customize this behavior.
v0.2.0 (2025-05-12)
See #1647 — usage makes sense as part of ModelResponse, and could be really useful in "messages" (really a sequence of requests and response). In this PR:
- Adds
usagetoModelResponse(field has a default factory ofUsage()so it'll work to load data that doesn't have usage) - changes the return type of
Model.requestto justModelResponseinstead oftuple[ModelResponse, Usage]
v0.1.0 (2025-04-15)
See #1248 — the attribute/parameter name result was renamed to output in many places. Hopefully all changes keep a deprecated attribute or parameter with the old name, so you should get many deprecation warnings.
See #1484 — format_as_xml was moved and made available to import from the package root, e.g. from pydantic_ai import format_as_xml.