{"schemaVersion":"1.0","type":"TechArticle","types":["Article","TechArticle"],"slug":"type-hints-pydantic-making-your-code-and-your-data-check-themselves-salwp","url":"https://zyvop.com/type-hints-pydantic-making-your-code-and-your-data-check-themselves-salwp","title":"Type Hints & Pydantic: Making Your Code and Your Data Check Themselves","subtitle":"Learn what Python type hints really do, why they don’t enforce anything, and how Pydantic turns messy model replies into clean, validated data you can trust.","tldr":"Type hints help your editor and your teammates, but Python ignores them at runtime. This post explains the hints you’ll use most, then introduces Pydantic, the library that checks data for real and powers structured outputs from language models.","keywords":["Python","AI Engineer","Type Hints","Pydantic","Structured Outputs","AI","Zero to Top AI Engineer (2026): Full Series Plan"],"entities":["Pradeep Kumar","Python","AI Engineer","Type Hints","Pydantic","Structured Outputs","AI","Zero to Top AI Engineer (2026): Full Series Plan","ZyVOP"],"keyTakeaways":["In the last post you saw lines like role: str and -&gt; str, and I asked you to treat them as labels.","This post explains them properly.","It's core, because modern AI code is full of them, and because the second half, a library called Pydantic, is how you turn a model's messy text reply into clean, reliable data."],"headings":["Type hints: labels, not rules","The hints you'll use most","How much should you use?","The gap: hints don't check real data","Pydantic: data that checks itself","Adding rules with Field","Optional fields, lists, and nested models","Your own checks","Getting data out","Why this matters so much for LLM apps","Dataclass or Pydantic?","Mini project: a reply checker with feedback","Make it yours","Practice","What to take away"],"outboundLinks":[],"contentText":"In the last post you saw lines like role: str and -&gt; str, and I asked you to treat them as labels. This post explains them properly. It's core, because modern AI code is full of them, and because the second half, a library called Pydantic, is how you turn a model's messy text reply into clean, reliable data. There are two halves, and the difference between them is the whole lesson: Type hints are labels that help humans and tools. Python ignores them when your program runs. Pydantic actually checks your data while the program runs. Type hints: labels, not rules A type hint says what kind of value something should be: def add(a: int, b: int) -&gt; int: return a + bRead it as \"add takes two integers and gives back an integer.\" Now watch what happens when we ignore that: print(add(2, 3)) # 5 print(add(\"x\", \"y\")) # xyPython happily ran it. The hints didn't stop anything. They're just notes attached to the function. So what are they good for? Three things. Readers. Anyone looking at the function knows what goes in and what comes out, without hunting through the code. Your editor. It uses hints for autocomplete and for the little popups that show what a function expects. That alone saves a lot of time. Type checkers. Tools like mypy and pyright read your hints and find mistakes before you run anything. Given the call add(\"x\", \"y\"), a checker would report something like Argument 1 to \"add\" has incompatible type \"str\"; expected \"int\". Catching that bug at your desk beats finding it in production. The hints you'll use most For basic values, use the type names you already know: int, float, str, bool. For collections, say what's inside the brackets: count: int = 0 names: list[str] = [\"a\", \"b\"] scores: dict[str, float] = {\"a\": 0.5} point: tuple[int, int] = (3, 4) tags: set[str] = {\"x\"}The : int after a variable name works just like it does on a function parameter. For \"this or that\", use the pipe symbol |: def find_user(user_id: int) -&gt; str | None: users = {1: \"Asha\"} return users.get(user_id)str | None means \"a string, or nothing.\" You'll write that one constantly, since lots of functions sometimes find nothing. It also works for other combinations, like int | str. (If you read older tutorials, you'll see Optional[str] and List[str] imported from typing. That's the old style. In modern Python, the versions above are simpler.) Three more that matter for AI code: A function as an input, using Callable. The first part in brackets is the list of inputs, and the second is the output: from typing import Callable def apply(func: Callable[[int], int], value: int) -&gt; int: return func(value) print(apply(lambda n: n * 2, 21)) # 42A generator, using Iterator. Remember read_lines from Post 7? A function with yield hands back a stream of values, so the hint describes what each value is: from typing import Iterator def read_lines(path: str) -&gt; Iterator[str]: ...A fixed set of allowed values, using Literal: from typing import Literal def set_role(role: Literal[\"user\", \"assistant\"]) -&gt; str: return f\"role={role}\"A type checker will complain if you pass \"robot\". And in a minute you'll see how Pydantic uses the very same idea for real checking. How much should you use? Don't try to annotate every single variable. A good starting rule: put hints on function parameters and return values, and on the fields of data classes. That's where they help most. Python can usually work out the types of ordinary local variables on its own. The gap: hints don't check real data Here's the problem hints can't solve. Data from outside your program is untrusted. A web request. A file. A user typing something. And especially a language model's reply. You can write age: int, but if the JSON coming in says \"age\": \"banana\", Python won't object until something breaks three functions later. We saw this in the last post too: a dataclass with role: str accepted Message(1, 2) without complaint. That's where Pydantic comes in. Pydantic: data that checks itself Pydantic is a library that reads your type hints and enforces them. It's one of the most widely used libraries in modern Python, and it sits underneath many AI tools and web frameworks. Install it with: pip install pydantic(Post 12 explains virtual environments, which is the tidier way to install things. For now, this is fine.) This post uses Pydantic version 2, the current major version. If you find tutorials using .dict() and .json(), they're written for version 1, and the methods below have newer names. Here's a model: from pydantic import BaseModel class User(BaseModel): name: str age: intYou create one the same way as a dataclass: user = User(name=\"Asha\", age=29) print(user.name, user.age) # Asha 29Now the useful part. Pydantic converts values when it sensibly can: user = User(name=\"Asha\", age=\"29\") print(user.age) # 29 (it became a real integer)And it refuses values it can't make sense of: from pydantic import ValidationError try: User(name=\"Asha\", age=\"abc\") except ValidationError as error: print(error)The message looks something like this (shortened): 1 validation error for User age Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='abc', input_type=str]Look how clear that is. It tells you which field, what was wrong, and what value it received. That readability is a big reason people love it. A missing field is caught too: User(name=\"Asha\") # ValidationError: age, Field requiredAdding rules with Field You can add limits using Field: from pydantic import BaseModel, Field class ModelSettings(BaseModel): name: str temperature: float = Field(default=0.7, ge=0, le=2) max_tokens: int = Field(default=500, gt=0)ge means \"greater than or equal to,\" le means \"less than or equal to,\" and gt means \"greater than.\" Try ModelSettings(name=\"x\", temperature=5) and you'll get an error saying the input should be less than or equal to 2. Optional fields, lists, and nested models Models can be built from other models, which is how you describe real-world data: class Item(BaseModel): name: str qty: int = 1 class Order(BaseModel): order_id: int items: list[Item] note: str | None = None order = Order(order_id=7, items=[{\"name\": \"pen\", \"qty\": 2}, {\"name\": \"book\"}]) print(order.items[0].name) # pen print(order.items[1].qty) # 1 (the default)Notice that we passed plain dictionaries for the items, and Pydantic turned them into Item objects for us. Your own checks When a built-in rule isn't enough, write a validator. It's a decorator on a method: from pydantic import BaseModel, field_validator class Tag(BaseModel): label: str @field_validator(\"label\") @classmethod def clean_label(cls, value): value = value.strip().lower() if not value: raise ValueError(\"label can't be empty\") return value print(Tag(label=\" Python \").label) # pythonYes, that's two decorators stacked, plus @classmethod from the last post. By now, you've seen every piece of it. Getting data out user = User(name=\"Asha\", age=29) print(user.model_dump()) # {'name': 'Asha', 'age': 29} print(user.model_dump_json()) # {\"name\":\"Asha\",\"age\":29}And going the other way, from a dictionary or a JSON string: User.model_validate({\"name\": \"Asha\", \"age\": 29}) User.model_validate_json('{\"name\": \"Asha\", \"age\": 29}')Why this matters so much for LLM apps A language model produces text. But your app usually wants data: a title, a category, a list of steps, an amount. If the model writes a friendly paragraph and your code needs a field called category, you're stuck. The standard solution is structured output: You describe the shape you want, as a Pydantic model. You hand the model that shape. Pydantic can turn any model into a JSON Schema, which is a standard way of describing JSON, using Course.model_json_schema(). Several model libraries also accept the Pydantic class directly. The model replies with JSON that follows the shape. You validate the reply with model_validate_json, so the rest of your code receives clean, checked data. Step 4 is the one beginners skip, and it's the one that saves you. Even with a schema, models sometimes produce a value that's missing, misspelled, or out of range. Validation catches that in one place, with a clear message. One more honest note: validation checks the shape of the data, not whether it's true. A reply can be perfectly valid and still be wrong. We'll deal with that in the evals posts. You'll also meet Pydantic in FastAPI, which uses it to check incoming requests. We'll use that in Post 28. Dataclass or Pydantic? Dataclass Pydantic model Checks types when created No Yes Converts values (like \"29\" to 29) No Yes Reads and writes JSON Not built in Built in Part of Python itself Yes No, you install it Best for Simple internal data you control Data coming from outside, like APIs and model replies A good rule: use a dataclass for data you create yourself inside your program. Use Pydantic for data that comes from outside it. Mini project: a reply checker with feedback Let's build the core of a structured-output loop, using fake model replies so you don't need an API key. The first reply has a mistake, and the second is correct. We'll show the error message that you'd send back to the model so it can fix itself. Create reply_checker.py: from typing import Literal from pydantic import BaseModel, Field, ValidationError class Course(BaseModel): title: str difficulty: Literal[\"beginner\", \"intermediate\", \"advanced\"] hours: float = Field(gt=0) topics: list[str] = Field(default_factory=list) replies = [ '{\"title\": \"Intro to Python\", \"difficulty\": \"easy\", \"hours\": 3.5}', '{\"title\": \"Intro to Python\", \"difficulty\": \"beginner\", \"hours\": 3.5, \"topics\": [\"variables\", \"loops\"]}', ] for attempt, raw in enumerate(replies, start=1): try: course = Course.model_validate_json(raw) except ValidationError as error: print(f\"Attempt {attempt} failed. Feedback for the model:\") print(error) continue print(f\"Attempt {attempt} worked: {course.title} ({course.difficulty}, {course.hours} hours)\") breakRun it. On the first attempt, \"easy\" isn't one of the allowed difficulty levels, so you'll see an error along the lines of \"Input should be 'beginner', 'intermediate' or 'advanced'.\" The second reply passes, and you'll see: Attempt 2 worked: Intro to Python (beginner, 3.5 hours)In a real app, the loop would send that error text back to the model with a message like \"your last reply had these problems, please fix them,\" and ask again. That pattern (validate, show the model what went wrong, retry) is one of the most useful tricks in AI engineering. It also reuses the retry idea from Posts 6 and 8. Also try print(Course.model_json_schema()) to see the schema that you'd hand to a model. Make it yours Add a summary field that must be at most 200 characters (look up max_length in the Pydantic docs). Write a validator that strips extra spaces and capitalizes the title. Add a third reply that's invalid JSON (for example, with a missing quote) and see what error you get. Turn the loop into a function get_course(replies, max_attempts=3) that returns a Course or raises an error after too many failures. Practice About 20 minutes of real effort on each before asking for help. Add type hints to three functions you wrote in earlier posts, such as retry, batches, and make_multiplier. Write a function that takes a Callable[[str], str] and a list of strings, and returns the list with the function applied. Create a Person Pydantic model with a name, an age that can't be negative, and an optional email. Test it with good and bad data. Add a field_validator to Person that makes sure the name isn't empty. Parse this JSON string into a model with a nested model: {\"order_id\": 7, \"items\": [{\"name\": \"pen\", \"qty\": 2}]}. Then ask an AI tutor to review your code, not rewrite it. What to take away Type hints are labels. Python ignores them when running, but your editor and type checkers use them to help you and catch bugs early. Put hints on function parameters, return values, and data fields, and don't worry about the rest. str | None, list[str], Callable, Iterator, and Literal cover most of what you'll need. Pydantic enforces your types at runtime, converts values when it sensibly can, and gives clear errors when it can't. Structured outputs depend on this: describe the shape, ask for it, then validate what comes back. Use dataclasses for data you control, and Pydantic for data that comes from outside. Previous: Post 9, OOP the Practical Way Next in the series: Post 11, Errors, Logging &amp; Debugging, where we learn to handle failures, read stack traces, and find bugs faster.","contentHash":"sha256:6e22c3abefdc94455a6f90f4bf49a2a9a51cbe76ab0e33876c2e5a2f04483f71","authorName":"Pradeep Kumar","authorUrl":"https://zyvop.com/author/pradeep","authorSameAs":[],"category":"AI","tags":["Python","AI Engineer","Type Hints","Pydantic","Structured Outputs"],"audience":"Software engineers and developers building applications with AI","tone":"Practical and evidence-based engineering guidance","readingTimeMinutes":9,"wordCount":2080,"faqs":null,"primaryTopic":"AI","publishedAt":"2026-10-18T07:40:00.117Z","updatedAt":"2026-10-17T13:09:51.332Z","canonicalUrl":"https://zyvop.com/type-hints-pydantic-making-your-code-and-your-data-check-themselves-salwp"}