Skip to content

Chapter 3.3: Data Models with Pydantic

Time to learn: 50 minutes


1. Pydantic: The "Digital Blueprint" of a Spaceship

Imagine you are building a spaceship. You can't just weld pieces of metal together randomly. You need a detailed blueprint that defines:

  • The ship's name (type: string, max length: 50 characters)
  • The launch year (type: integer)
  • Whether it has a hyperdrive (type: yes/no)

Pydantic is a library that allows you to create such "digital blueprints" for your data in Python. In FastAPI, it performs three key functions:

  1. Structure Declaration: Clearly describes the fields your data consists of.
  2. Data Validation: Automatically checks if incoming data conforms to the blueprint.
  3. Documentation: FastAPI uses these blueprints to create detailed and interactive documentation.

💡 Space Analogy: A Pydantic model is the technical passport of an object. Any "cargo" (data) arriving at the station must match the specifications in the passport. If not, the onboard computer (Pydantic) will reject it.


2. Creating the First Blueprint: The Spaceship Model

Let's create a model that will describe our spacecraft.

Step 1: Import BaseModel from Pydantic Pydantic is already installed with fastapi[all]. We just need to import the base class for our models.

Add this to the top of main.py, next to the other imports:

# main.py
from fastapi import FastAPI
from pydantic import BaseModel

Step 2: Describe the Spaceship model Create a class that inherits from BaseModel. Inside the class, define the fields and their types using standard Python type hints.

Add this code to main.py (you can place it after the imports):

class Spaceship(BaseModel):
    """
    The technical passport (model) of a spaceship.
    """
    name: str
    type: str
    launch_year: int
    status: str
That's it! You have just created a "blueprint." Pydantic now knows that any object of type Spaceship must have four fields with the specified types.


3. Applying the Model: Improving Our Endpoints

Now let's use our new model to make the API "smarter."

A. Model as a Response (Response Model) We can tell FastAPI that our endpoint should return data that conforms to the Spaceship model. This ensures that the response will always have the correct structure.

Modify the /spaceships/{ship_id} endpoint as follows:

# main.py

# ... code with db_spaceships and the Spaceship model ...

# Use `response_model` to specify the "blueprint" for the response
@app.get("/spaceships/{ship_id}", response_model=Spaceship)
def get_spaceship(ship_id: int):
    """
    Returns data about a ship, conforming to the Spaceship model.
    """
    ship = db_spaceships.get(ship_id)
    return ship
- response_model=Spaceship: We are telling FastAPI: "The response of this function must conform to the Spaceship structure. Filter out any extra fields and make sure the types are correct."

What does this give us?

  • Data Filtering: If db_spaceships had extra fields (e.g., "secret_code"), they would not be included in the final JSON.
  • Structure Guarantee: The API client can be sure that it will always receive a response in the expected format.
  • Documentation: The /docs page will now show a precise example of the response (Example Value).

B. Models for Collections What about the /spaceships endpoint, which returns a list of ships? For this, we need to use List from the typing module.

Modify the imports and the /spaceships endpoint:

# main.py at the top
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List  # <-- Import List

# ... code ...

# Specify that the response is a list (List) of objects of type Spaceship
@app.get("/spaceships", response_model=List[Spaceship])
def get_spaceships():
    """
    Returns a list of ships. Each item in the list
    is validated against the Spaceship model.
    """
    # Pydantic cannot work with a dictionary whose keys are IDs.
    # We convert our dictionary to a simple list.
    return list(db_spaceships.values())

  • response_model=List[Spaceship]: We are specifying that the response will be a list where each element is an object conforming to the Spaceship model.
  • return list(db_spaceships.values()): An important change! Pydantic expects an iterable object (a list), not a dictionary where the keys are IDs. We convert the values of our "DB simulator" into a list.

4. Checking the Improved API

Make sure the uvicorn server is running with --reload.

  1. Check http://127.0.0.1:8000/spaceships: The response is now a JSON array, not an object. This is a more correct and standard structure for collections.
    [
      { "name": "Voyager-1", "type": "Probe", ... },
      { "name": "Hubble Space Telescope", ... }
    ]
    
  2. Check http://127.0.0.1:8000/spaceships/1: The response has not changed, but it is now guaranteed to conform to the model.
  3. Take a look at /docs: In the "Schemas" section at the bottom of the page, your Spaceship model has appeared. And the example responses for the endpoints now display a beautiful, structured data schema.

5. Advanced Validation: The "Onboard Computer" in Action

Pydantic can do much more than just check types.

Let's add validation to our Spaceship model:

from pydantic import BaseModel, Field

class Spaceship(BaseModel):
    name: str = Field(..., min_length=3, max_length=50, description="The name of the ship")
    type: str
    launch_year: int = Field(..., gt=1950, description="Launch year must be after 1950")
    status: str

  • Field(...): Used to add additional validation rules.
  • ... (Ellipsis): Means that the field is required.
  • min_length, max_length: Constraints for a string.
  • gt: "Greater Than".

Although we are not yet creating new ships (that will be in the next chapter), these rules will already be reflected in the documentation and will work when we implement POST requests.


Review Quiz

1. Pydantic in FastAPI is used for...

2. To create a data model, you need to inherit a class from...

3. The `response_model` parameter in the `@app.get` decorator is needed to...

4. How do you specify that an endpoint returns a *list* of objects of type `Item`?

5. `Field(..., gt=0)` in a Pydantic model means that the field is...


🚀 Chapter Summary:

You have designed the "digital blueprints" for your API's data. Now it not only works, but it works predictably and reliably.

  • 📝 A Spaceship model has been created using Pydantic.
  • 🛡️ The API now validates and filters outgoing data using response_model.
  • 📊 The documentation has become much more informative, showing exact data schemas.

The blueprints are ready and approved! In the next chapter, we will move from reading data to creating it—implementing full CRUD operations for our fleet.

📌 Checkpoint:

  • Make sure the Spaceship model schema has appeared in /docs.
  • Check that the /spaceships endpoint now returns a JSON array ([...]) and not an object ({...}).
  • Make sure there are no syntax errors in the code after adding the models.

⚠️ If you have errors:

  • NameError: name 'BaseModel' is not defined: Check that you have imported BaseModel from pydantic.
  • NameError: name 'List' is not defined: Check that you have imported List from typing.
  • The response for /spaceships is empty ([]): Make sure you have changed return db_spaceships to return list(db_spaceships.values()).