# validation ## annotated types as a type library the most important pattern in pydantic. instead of repeating `@field_validator` on every model, bind validation to the type itself: ```python from typing import Annotated from pydantic import AfterValidator, BeforeValidator, Field NonNegativeInteger = Annotated[int, Field(ge=0)] PositiveInteger = Annotated[int, Field(gt=0)] StatusCode = Annotated[int, Field(ge=100, le=599)] LogLevel = Annotated[ Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], BeforeValidator(lambda x: x.upper()), ] ``` now any model field typed as `StatusCode` gets validation for free. the validator travels with the type. this scales into a full type library: ```python # types/__init__.py from .names import Name, VariableName, URILike from ._datetime import DateTime, PositiveInterval NonNegativeFloat = Annotated[float, Field(ge=0.0)] LaxUrl = Annotated[str, BeforeValidator(lambda x: str(x).strip())] ``` from [prefect/types/\_\_init\_\_.py](https://github.com/prefecthq/prefect/blob/main/src/prefect/types/__init__.py) ## BeforeValidator vs AfterValidator - `BeforeValidator` — runs before pydantic's own parsing. use for coercion (strings to timedeltas, env var parsing): ```python def _convert_seconds_to_timedelta(value: Any) -> Any: if isinstance(value, timedelta): return value if isinstance(value, (int, float)): return timedelta(seconds=value) return value SecondsTimeDelta = Annotated[timedelta, BeforeValidator(_convert_seconds_to_timedelta)] ``` - `AfterValidator` — runs after pydantic parses. use for constraints on already-typed values: ```python def _validate_non_negative_timedelta(v: timedelta) -> timedelta: if v.total_seconds() < 0: raise ValueError("must be non-negative") return v NonNegativeTimedelta = Annotated[timedelta, AfterValidator(_validate_non_negative_timedelta)] ``` ## composing validators `Annotated` stacks. you can layer multiple validators and field constraints: ```python ValidAssetKey = Annotated[ str, AfterValidator(validate_valid_asset_key), Field( max_length=512, description="URI-like string with restricted characters", ), ] ``` ## domain-specific types in their own module when a domain has enough types, extract them. prefect's `names.py` defines `Name`, `VariableName`, `BlockDocumentName`, `URILike` — all `Annotated` types with validators using `functools.partial` for parameterization: ```python from functools import partial def raise_on_name_alphanumeric_dashes_only( value: str | None, field_name: str = "value" ) -> str | None: if value is not None and not re.match(r"^[a-z0-9-]*$", value): raise ValueError(f"{field_name} must only contain lowercase letters, numbers, and dashes.") return value BlockDocumentName = Annotated[ Name, AfterValidator(partial(raise_on_name_alphanumeric_dashes_only, field_name="Block document name")), ] ``` `partial` lets one validator function serve multiple domain types with customized error messages. from [prefect/types/names.py](https://github.com/prefecthq/prefect/blob/main/src/prefect/types/names.py) ## model_validator for side effects run setup code when a model is instantiated: ```python from typing import Self from pydantic import model_validator from pydantic_settings import BaseSettings class Settings(BaseSettings): debug: bool = False @model_validator(mode="after") def configure_logging(self) -> Self: setup_logging(debug=self.debug) return self settings = Settings() # logging configured on import ``` the validator runs after all fields are set. use for side effects that depend on configuration values. from [bot/config.py](https://github.com/zzstoatzz/bot) ## env var parsing with BeforeValidator for settings that come from environment variables as strings but need richer types: ```python def validate_set_T_from_delim_string( value: str | T | set[T] | None, type_: Any, delim: str | None = None ) -> set[T]: """parse comma-delimited env vars into typed sets. PREFECT_CLIENT_RETRY_EXTRA_CODES=429,502,503 → {429, 502, 503} """ if not value: return set() T_adapter = TypeAdapter(type_) delim = delim or "," if isinstance(value, str): return {T_adapter.validate_strings(s.strip()) for s in value.split(delim)} ... ClientRetryExtraCodes = Annotated[ str | StatusCode | set[StatusCode] | None, BeforeValidator(partial(validate_set_T_from_delim_string, type_=StatusCode)), ] ``` `TypeAdapter` is useful here — it validates individual values without needing a full model. from [prefect/types/\_\_init\_\_.py](https://github.com/prefecthq/prefect/blob/main/src/prefect/types/__init__.py) sources: - [coping with python's type system](https://blog.zzstoatzz.io/coping-with-python-type-system/) - [prefect/types/](https://github.com/prefecthq/prefect/tree/main/src/prefect/types)