AlgoMaster Logo

Type Hints and Pydantic

9 min readUpdated June 22, 2026
Listen to this chapter
Unlock Audio

AI systems move data across boundaries you do not fully control: user input, provider APIs, JSON files, queues, databases, tool calls, and model-generated structured output. Type hints make the expected shape of that data visible in code. Pydantic checks real data at runtime.

Type hints do not make Python a statically typed language. They document intent, improve editor help, and let tools such as mypy or pyright catch many mistakes before you run the code. Pydantic uses similar annotations to validate actual input, which is what you need when a request body, environment variable, or model response may not match your expectations.

This chapter covers practical type hints and Pydantic v2 patterns for AI applications.

Python Type Hints: The Basics

Type hints were introduced in Python 3.5 through PEP 484. They are optional, and ordinary Python does not reject a value just because it violates an annotation.

They are still worth using because your tools understand them. IDEs use type hints for autocomplete and inline help. Static analyzers use them to catch likely mistakes before runtime. Libraries such as Pydantic and FastAPI use them as the basis for validation and API schemas.

Annotating Variables and Functions

The syntax is straightforward. A colon after a variable or parameter name, followed by the type:

main.py
Loading...

The -> str after the parameter list is the return type annotation. It tells readers and tools what the function is expected to return.

Collection Types

For lists, dicts, sets, and tuples, annotate the contents too. Since Python 3.9, you can use the built-in collection types directly.

main.py
Loading...

The key difference from untyped code is that list[str] tells tools every element should be a string. That helps autocomplete, static checks, and reviews.

Optional, Union, and None Types

In production AI code, values are often missing. A model response might omit a field. A configuration value might be unset. You need a way to say, "this may be a string, or it may be None."

Optional

Optional[str] means str or None.

main.py
Loading...

Union

Optional[str] is shorthand for Union[str, None]. Union is more general: it works with any combination of accepted types.

main.py
Loading...

Python 3.10+ Syntax

Starting with Python 3.10, you can use the pipe operator | instead of Union. It is shorter and easier to read:

main.py
Loading...

Use the | syntax when your project targets Python 3.10 or newer. Use Optional and Union when you need compatibility with older Python versions.

Type Aliases, TypeVar, and Generics

As annotations get more complex, they can start to hide the code they are meant to clarify. Type aliases and generics help keep them readable.

Type Aliases

A type alias gives a name to a type. Use it when the same shape appears in several places:

main.py
Loading...

In Python 3.12 and newer, the type statement is the newer way to define aliases. In code that still targets Python 3.10 or 3.11, TypeAlias remains common. AI code often creates aliases for chat messages, embedding vectors, token sequences, and similar repeated shapes:

main.py
Loading...

TypeVar and Generics

TypeVar lets you write reusable functions while preserving a relationship between input and output types.

main.py
Loading...

You can also constrain a TypeVar to specific types:

main.py
Loading...

Generic Classes

You can also create generic classes. This pattern shows up in libraries that return the same kind of container for different data types:

main.py
Loading...

You do not need to master generics on day one. It is enough to recognize the pattern when you see it in SDKs, frameworks, and type checker output.

Pydantic: Runtime Validation from Type Hints

Type hints alone are advisory. Pydantic changes that at the boundary where you instantiate or validate a model. It reads your annotations, checks incoming data, applies its configured parsing rules, and raises structured errors when something does not match.

That is why Pydantic appears so often in Python AI systems. FastAPI uses it for request and response models. AI SDKs and orchestration libraries use it for configuration, tool schemas, and structured outputs. If data crosses a trust boundary, a Pydantic model is often a good place to validate it.

Defining a Model

A Pydantic model is a class that inherits from BaseModel. Each field is a class attribute with a type annotation:

main.py
Loading...

So far, this looks similar to a dataclass. The difference shows up when incoming data does not match the declared types.

Automatic Validation and Coercion

Pydantic validates each field against its type annotation. If data does not match, you get a detailed error. By default, Pydantic also parses common input forms, such as "1024" into 1024 for an integer field:

main.py
Loading...

This matters because AI applications receive data from many sources: users, API responses, config files, environment variables, queues, databases, and model outputs. Each source has its own quirks. Pydantic gives you one place to parse and validate that data before the rest of the application trusts it.

Here is how the validation pipeline works:

Raw input enters from the left. Pydantic checks fields and applies parsing rules. If everything passes, you get a model instance with typed attributes. If not, you get a ValidationError that points to the field that failed. Once you have a model instance, you can serialize it back to a dict or JSON string.

Field() for Constraints and Metadata

The Field() function gives you more control over a field: defaults, validation constraints, descriptions, and schema metadata.

main.py
Loading...

Common Field constraints:

Scroll
ConstraintApplies ToMeaning
ge, gtNumbersGreater than or equal / greater than
le, ltNumbersLess than or equal / less than
min_length, max_lengthStrings, ListsMinimum / maximum length
patternStringsRegex pattern the value must match
defaultAnyDefault value if not provided
descriptionAnyHuman-readable description (shows up in JSON Schema)

Descriptions are useful because they become part of the JSON Schema generated from the model. Structured-output systems can use that schema to describe the expected fields.

Nested Models

Real data is rarely flat. A model response may contain messages, tool calls, token usage, citations, or safety metadata. Pydantic handles nesting naturally: use one model as a field type inside another.

main.py
Loading...

You can pass raw dictionaries for nested models, and Pydantic converts them into model instances. This is especially useful when parsing JSON responses from APIs.

Custom Validators

Sometimes type checking and field constraints are not enough. You may need to normalize text, reject unsupported values, or check that two fields make sense together. Pydantic provides validators for that.

Field Validators

A @field_validator runs for one field. It receives the value and can return a cleaned value or raise an error:

main.py
Loading...

The validator returns the value Pydantic should store. If it raises a ValueError, Pydantic includes that message in the validation error.

Model Validators

A @model_validator can run on the whole model. This is useful for cross-field validation:

main.py
Loading...

The mode="after" validator runs after fields are validated and receives the model instance. Pydantic also supports mode="before" validators for working with raw input before field validation.

Serialization: Getting Data Out

Getting data into Pydantic models is only half the story. You also need to get data out as dictionaries for database inserts, JSON strings for API responses, or validated model instances from raw data.

model_dump() and model_dump_json()

main.py
Loading...

model_validate() and model_validate_json()

Going the other direction, use model_validate() for dictionaries and model_validate_json() for JSON strings:

main.py
Loading...

This pattern is common: receive JSON from an API, validate it into a model, work with typed attributes, then serialize it back out when needed.

Pydantic Settings for Configuration

AI applications have a lot of configuration: API keys, model names, temperature defaults, chunk sizes, database URLs, and vector-store endpoints. Pydantic Settings loads these values from environment variables and .env files with validation.

main.py
Loading...

This replaces scattered os.getenv() calls with one validated settings object. SecretStr hides the raw secret in logs and repr() output. If a required API key is missing, startup fails with a clear validation error instead of a None value surfacing later in the pipeline.

Your .env file looks like this:

Structured LLM Output Schema

A common AI engineering pattern is to define a Pydantic model for the structure you want back from an LLM, then use that schema for structured output and validation.

Here is an example: extracting structured information from a technical document.

main.py
Loading...

You can pass this schema to an LLM using a structured-output API. The field descriptions document the expected output, and the constraints (min_length, max_length) are checked when the response is parsed.

Here is how this model looks with the OpenAI Python SDK's Responses API:

main.py
Loading...

The SDK parses the model output into the DocumentAnalysis schema. Pydantic validates it, and your application receives a typed object with attribute access and serialization. You still need application-level handling for refusals, empty inputs, and domain-specific quality checks, but you are no longer parsing free-form text with regular expressions.

This pattern is useful for tool arguments, extraction pipelines, evaluators, and any workflow that expects structured model output.

Quiz

Type Hints and Pydantic Quiz

10 quizzes

References