Type hints and pydantic
By the end: You can annotate code so the editor helps you, and validate untrusted data at the boundary.
- Basic annotations and why they help
- Optional values, and the checker following your if statements
- Lists, dicts and unions
- pydantic for validating real data
- Generating a JSON schema from a model
New to python? A programming language is the set of written instructions a computer will follow, and Python is the one this field settled on. It reads close to English: a line that filters a list of records looks roughly like a sentence describing that filter. Every model provider ships a Python library, and nearly every machine learning tool is written in it. Why it matters
A type hint is a note in the code saying what kind of value belongs where. Python ignores hints while running, but your editor and a type checker read them, so a mistake surfaces as you type rather than in production. This section covers the annotations worth writing, then pydantic, which uses the same syntax to check data arriving from outside your program — an API response, a form, a model's output — and to reject it clearly when it is wrong.
What a type hint is#
A type hint says what kind of value something should be. Python ignores hints while running. Your editor and a type checker read them and warn you before you run anything.
def greet(name: str) -> str:
return f"Hello {name}"
count: int = 0
prices: list[float] = []
user: dict[str, str] = {}The practical benefit is autocomplete. Once your editor knows a variable is a str, it can offer you every string method. Without hints it can only guess.
Values that might be missing#
This is the most useful thing type hints do. If a function might return nothing, say so, and the checker will insist that callers handle it.
def find_user(user_id: str) -> dict | None:
...
user = find_user("abc")
print(user["name"]) # warning: user might be None
if user is not None:
print(user["name"]) # fine nowThe common shapes#
from typing import Literal
names: list[str]
scores: dict[str, int]
pair: tuple[int, int]
value: int | str # either one
role: Literal["user", "assistant"] # only these exact stringsWhere hints stop being enough#
Nothing checks a hint at runtime. If an API sends you a string where you declared an int, Python will not complain and your code will break later with a confusing error. For data arriving from outside your program, you need real validation.
pydantic#
pydantic takes a class definition and turns it into a validator. You describe the shape once, and it checks incoming data against it and converts types where it safely can.
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int
email: str | None = None # optional, defaults to None
user = User(name="Jack", age=29)
user.name # 'Jack'
User(name="Jack", age="29") # works: "29" converts to 29
User(name="Jack", age="abc") # raises ValidationErrorField adds rules the type alone cannot express.
from pydantic import BaseModel, Field
class Invoice(BaseModel):
number: str
amount: float = Field(gt=0) # must be positive
currency: str = Field(min_length=3, max_length=3)
overdue: bool = FalseWhen validation fails you get a list of every problem, not just the first, which makes it possible to tell a user everything that is wrong at once.
from pydantic import ValidationError
try:
invoice = Invoice.model_validate_json(raw_text)
except ValidationError as e:
for err in e.errors():
field = ".".join(str(p) for p in err["loc"])
print(f"{field}: {err['msg']}")Why this matters for AI work#
A model returns text. That text is untrusted, exactly like input from a stranger. A pydantic model is where it stops being a guess and becomes something you can rely on. pydantic can also produce a JSON schema from your class, and that schema is exactly what tool definitions and structured output requests need.
Invoice.model_json_schema()
# Send this to the API as the required output shape,
# then validate the response with the same class.Validate once, at the edge
Parse incoming data into a model as soon as it arrives, then pass the typed object through your program. Validating deeper down means checking the same dictionary in several places, and those checks slowly disagree with each other.
Where people go wrong
Assuming a type hint protects you. It documents intent, it does not check anything. The moment data comes from a file, an API or a model, you need a real validator.
If this comes up in an interview
Are Python type hints enforced at runtime?
No. Python ignores them while running. They exist for editors and type checkers, which is why data from an API still needs real validation with something like pydantic.
What does dict | None mean in a return type?
The function returns either a dictionary or None. A type checker will then warn if you use the result without checking for None first. Older syntax writes it as Optional[dict].
Why use pydantic rather than checking fields by hand?
It gives you validation, type conversion, clear error messages listing every problem, and a JSON schema, all from one class definition. Hand-written checks drift apart from the shape they are meant to enforce.
Where should validation happen in an application?
At the boundary, as soon as untrusted data arrives. After that point everything downstream can assume the data is the right shape, which keeps the checks in one place.
Build this: Define a pydantic model for a blog post with a title, an optional subtitle, a word count that must be positive, and a status limited to draft or published. Feed it three bad payloads and print a clear message for each.
Watch it explained
This is one section of Python, 33 hours across 9 sections. Read it in full to get the whole topic in order.
Stop 1 of 50 on the AI Engineer route
0 of 50 topics done · 0h behind you, 603h to go
What you have marked done is stored in this browser only. It does not reach a server, and clearing site data will clear it.