new·Earn with mozg — 20% of every monthSend somebody here and take a fifth of every plan payment they make, for as long as they keep paying — not a bounty on the first invoice. Your handle is the link, the window is thirty days, and the commission lands on your balance the second they pay. Free to join: if you have signed in, you already have the link. mozg.sh/earnall news →
mozg.beta
Sign in

Pydantic · Concepts · all subjects

unions

20 notes, read out of this brain and free to use. Each one was extracted from a source and is re-checked against its exam.

Three fundamental union validation modes

Pydantic supports three fundamental approaches to validating unions: left to right mode, smart mode, and discriminated unions. Left to right mode tries each member in order and returns the first match. Smart mode is the default in Pydantic >=2 and attempts to find the best match using metrics like number of valid fields set and exactness of the match. Discriminated unions use a discriminator to select which member to validate against, offering better performance and predictability.

Left to right union mode behavior

In left to right mode, validation is attempted against each member of the union in the order they are defined, and the first successful validation is accepted as input. If validation fails on all members, the validation error includes errors from all members. This mode must be set as a Field parameter using union_mode='left_to_right' and is not the default in Pydantic >=2 because it often leads to unexpected validation results.

Smart mode default union validation

In Pydantic >=2, union_mode='smart' is the default mode for union validation. Smart mode attempts to select the best match for the input from union members using two metrics: the number of valid fields set (relevant for models, dataclasses, and typed dicts, introduced in Pydantic v2.8.0) and exactness of the match (relevant for all types). The exact algorithm may change between Pydantic minor releases.

Exactness scoring in smart mode unions

In smart mode, exactness scoring for union member matches uses three categories from highest to lowest score: an exact type match (e.g., int input to a float | int union), validation that would have succeeded in strict mode, and validation that would have succeeded in lax mode. The union member with the highest exactness score is considered the best match.

Smart mode algorithm for BaseModel, dataclass, and TypedDict

For BaseModel, dataclass, and TypedDict union members in smart mode: union members are attempted left to right with successful matches scored into exactness categories and valid fields counted; after all members are evaluated, the member with the highest valid fields set count is returned; if there is a tie in valid fields count, exactness score is used as a tiebreaker; if validation failed on all members, all errors are returned.

Smart mode algorithm for other data types

For all other data types in smart mode union validation: union members are attempted left to right with successful matches scored into exactness categories; if validation succeeds with an exact type match, that member is returned immediately without attempting following members; if validation succeeded on at least one member as a strict match, the leftmost strict match is returned; if validation succeeded on at least one member in lax mode, the leftmost match is returned; if validation failed on all members, all errors are returned.

Discriminated unions with string discriminators

Discriminated unions can be validated efficiently by specifying a common field on union members that distinguishes which case the data should be validated against. To use this, define a common field on each model (e.g., pet_type) typed as accepting one or multiple literal values, then set the discriminator parameter of Field() or use the Discriminator type. This makes validation more efficient and avoids proliferation of errors when validation fails.

Callable discriminator for unions

When there is no single uniform field across all union members, a callable Discriminator can be used. The callable discriminator function must handle both dict and model type inputs, similar to mode='before' validators, because Pydantic uses callable discriminators for both validation and serialization. The function should return the discriminator value or None if not found.

Left to right mode with unexpected type coercion

When using union_mode='left_to_right' with int | str, validation of a numeric string like '456' will match the int member first in lax mode because the numeric string is valid as input to int. This results in the value being coerced to int instead of remaining a string, which is often unexpected. The order of union members is critical with this mode.

Discriminated unions with custom error handling

The Discriminator constructor accepts custom_error_type, custom_error_message, and custom_error_context parameters to customize error messages when validation fails. custom_error_type sets the type attribute of the ValidationError, custom_error_message sets the msg attribute, and custom_error_context sets the ctx attribute.

Nested discriminated unions

Multiple discriminators can be combined by creating nested Annotated types. Only one discriminator can be set for a field directly, but by nesting Annotated types with their own discriminators, you can validate complex union structures with multiple levels of discrimination.

Tag annotation for clearer union error messages

Union cases can be labeled with Tag annotations to make error messages clearer. When using Tag, the error output includes the tag label instead of showing the full complex type representation, making it easier to understand which union case failed.

Root models with Literal in discriminated unions

As of Pydantic v2.13, Root models with a Literal root type can be used in place of Literal types in discriminated unions.

Left to right union mode example

Example showing left to right union mode: from pydantic import BaseModel, Field, ValidationError class User(BaseModel): id: str | int = Field(union_mode='left_to_right') print(User(id=123)) #> id=123 print(User(id='hello')) #> id='hello' try: User(id=[]) except ValidationError as e: print(e) This demonstrates that with left to right mode, the first matching member (str in this case) is tried first, then int.

Smart mode union example with multiple types

Example showing smart mode union validation: from uuid import UUID from pydantic import BaseModel class User(BaseModel): id: int | str | UUID name: str user_01 = User(id=123, name='John Doe') print(user_01) #> id=123 name='John Doe' user_02 = User(id='1234', name='John Doe') print(user_02) #> id='1234' name='John Doe' user_03_uuid = UUID('cf57432e-809e-4353-adbd-9d5c0d733868') user_03 = User(id=user_03_uuid, name='John Doe') print(user_03) #> id=UUID('cf57432e-809e-4353-adbd-9d5c0d733868') name='John Doe' Smart mode correctly identifies the best match for each input type.

Discriminated union with string discriminator example

Example showing discriminated unions with string discriminators: from typing import Literal from pydantic import BaseModel, Field, ValidationError class Cat(BaseModel): pet_type: Literal['cat'] meows: int class Dog(BaseModel): pet_type: Literal['dog'] barks: float class Lizard(BaseModel): pet_type: Literal['reptile', 'lizard'] scales: bool class Model(BaseModel): pet: Cat | Dog | Lizard = Field(discriminator='pet_type') n: int print(Model(pet={'pet_type': 'dog', 'barks': 3.14}, n=1)) #> pet=Dog(pet_type='dog', barks=3.14) n=1 This shows how the discriminator field is used to select the correct union member.

Callable discriminator example

Example showing callable Discriminator for unions: from typing import Annotated, Any, Literal from pydantic import BaseModel, Discriminator, Tag class Pie(BaseModel): time_to_cook: int num_ingredients: int class ApplePie(Pie): fruit: Literal['apple'] = 'apple' class PumpkinPie(Pie): filling: Literal['pumpkin'] = 'pumpkin' def get_discriminator_value(v: Any) -> str | None: if isinstance(v, dict): return v.get('fruit', v.get('filling')) return getattr(v, 'fruit', getattr(v, 'filling', None)) class ThanksgivingDinner(BaseModel): dessert: Annotated[ Annotated[ApplePie, Tag('apple')] | Annotated[PumpkinPie, Tag('pumpkin')], Discriminator(get_discriminator_value), ] apple_variation = ThanksgivingDinner.model_validate( {'dessert': {'fruit': 'apple', 'time_to_cook': 60, 'num_ingredients': 8}} ) print(repr(apple_variation)) #> ThanksgivingDinner(dessert=ApplePie(time_to_cook=60, num_ingredients=8, fruit='apple')) This shows how a callable discriminator handles both dict and model type inputs.

Nested discriminated unions example

Example showing nested discriminated unions: from typing import Annotated, Literal from pydantic import BaseModel, Field, ValidationError class BlackCat(BaseModel): pet_type: Literal['cat'] color: Literal['black'] black_name: str class WhiteCat(BaseModel): pet_type: Literal['cat'] color: Literal['white'] white_name: str Cat = Annotated[BlackCat | WhiteCat, Field(discriminator='color')] class Dog(BaseModel): pet_type: Literal['dog'] name: str Pet = Annotated[Cat | Dog, Field(discriminator='pet_type')] class Model(BaseModel): pet: Pet n: int m = Model(pet={'pet_type': 'cat', 'color': 'black', 'black_name': 'felix'}, n=1) print(m) #> pet=BlackCat(pet_type='cat', color='black', black_name='felix') n=1 This demonstrates combining multiple discriminators through nested Annotated types.

Callable discriminator with primitive types

Example showing callable Discriminator with union of models and primitive types: from typing import Annotated, Any from pydantic import BaseModel, Discriminator, Tag, ValidationError def model_x_discriminator(v: Any) -> str | None: if isinstance(v, int): return 'int' if isinstance(v, (dict, BaseModel)): return 'model' else: return None class SpecialValue(BaseModel): value: int class DiscriminatedModel(BaseModel): value: Annotated[ Annotated[int, Tag('int')] | Annotated['SpecialValue', Tag('model')], Discriminator(model_x_discriminator), ] model_data = {'value': {'value': 1}} m = DiscriminatedModel.model_validate(model_data) print(m) #> value=SpecialValue(value=1) int_data = {'value': 123} m = DiscriminatedModel.model_validate(int_data) print(m) #> value=123 This shows how callable discriminators can handle unions of both model and primitive types.

Recommendation for union validation modes

Pydantic documentation recommends using discriminated unions in general, as they are more performant and more predictable than untagged unions. For complex cases with untagged unions, it is recommended to use union_mode='left_to_right' if you need guarantees about the order of validation attempts. For incredibly specialized behavior, a custom validator can be used.

Give your agent this brain