Mathew K Analytics

Lesson 4 · FastAPI deep dive

FastAPI Tutorial #4: Response Models & HTTP Status Codes

Video four of the eighteen-part series: controlling exactly what a client actually sees back. responsemodel, filtering fields, and choosing the right status…

⬇ Download notebookOpen in Colab ↗

What you'll learn

Data

No separate download needed — the notebook creates or downloads everything it uses.

📓 Full notebook

Download .ipynb

FastAPI Deep-Dive, Video 4: Response Models and Status Codes#

  • Video four of the eighteen-part series: controlling exactly what a client actually sees back.
  • response_model, filtering fields, and choosing the right status code.
  • Let's get into it.

Part 1: The Problem - Leaking Internal Fields#

from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel
app = FastAPI()
class UserInternal(BaseModel):
    username: str
    password: str
@app.post('/signup-unsafe')
def signup_unsafe(user: UserInternal):
    return user
client = TestClient(app)
print(client.post('/signup-unsafe', json={'username': 'sam', 'password': 'secret123'}).json())
{'username': 'sam', 'password': 'secret123'}

Part 2: response_model Basics#

class UserOut(BaseModel):
    username: str
@app.post('/signup', response_model=UserOut)
def signup(user: UserInternal):
    return user.model_dump()
response = client.post('/signup', json={'username': 'sam', 'password': 'secret123'})
print(response.json())
{'username': 'sam'}

Part 3: response_model Filters Extra Fields Automatically#

class ItemOut(BaseModel):
    name: str
    price: float
@app.get('/leaky-item', response_model=ItemOut)
def leaky_item():
    return {'name': 'Mug', 'price': 9.99, 'internal_cost': 3.2, 'secret_note': 'do not expose'}
print(client.get('/leaky-item').json())
{'name': 'Mug', 'price': 9.99}

Part 4: response_model_exclude_unset#

class Settings(BaseModel):
    theme: str = 'light'
    notifications: bool = True
    language: str = 'en'
@app.post('/settings', response_model=Settings, response_model_exclude_unset=True)
def update_settings(settings: Settings):
    return settings
print(client.post('/settings', json={'theme': 'dark'}).json())
{'theme': 'dark'}

Part 5: response_model with a List#

@app.get('/items', response_model=list[ItemOut])
def list_items():
    return [
        {'name': 'Mug', 'price': 9.99, 'internal_cost': 3.2},
        {'name': 'Plate', 'price': 14.5, 'internal_cost': 4.1}
    ]
print(client.get('/items').json())
[{'name': 'Mug', 'price': 9.99}, {'name': 'Plate', 'price': 14.5}]

Part 6: More Status Codes - 204 and 404#

from fastapi import HTTPException, status
fake_db = {1: 'Mug', 2: 'Plate'}
@app.delete('/items/{item_id}', status_code=status.HTTP_204_NO_CONTENT)
def delete_item(item_id: int):
    fake_db.pop(item_id, None)
    return None
@app.get('/items/{item_id}/name')
def get_item_name(item_id: int):
    if item_id not in fake_db:
        raise HTTPException(status_code=404, detail='item not found')
    return {'name': fake_db[item_id]}
print(client.delete('/items/1').status_code)
print(client.get('/items/1/name').status_code)
204
404

Part 7: Multiple Documented Response Types#

class ErrorOut(BaseModel):
    detail: str
@app.get(
    '/items/{item_id}/priced',
    response_model=ItemOut,
    responses={404: {'model': ErrorOut, 'description': 'Item not found'}}
)
def get_priced_item(item_id: int):
    if item_id != 1:
        raise HTTPException(status_code=404, detail='not found')
    return {'name': 'Mug', 'price': 9.99}
print(app.openapi()['paths']['/items/{item_id}/priced']['get']['responses'].keys())
dict_keys(['200', '404', '422'])

Part 8: response_model_exclude and response_model_include#

class FullProfile(BaseModel):
    username: str
    email: str
    internal_id: int
@app.get(
    '/profile',
    response_model=FullProfile,
    response_model_exclude={'internal_id'}
)
def get_profile():
    return {'username': 'sam', 'email': 'sam@example.com', 'internal_id': 999}
print(client.get('/profile').json())
{'username': 'sam', 'email': 'sam@example.com'}

Part 9: Returning a Model Instance vs a Dict#

@app.get('/as-dict', response_model=ItemOut)
def as_dict():
    return {'name': 'Cup', 'price': 3.0}
@app.get('/as-instance', response_model=ItemOut)
def as_instance():
    return ItemOut(name='Cup', price=3.0)
print(client.get('/as-dict').json())
print(client.get('/as-instance').json())
{'name': 'Cup', 'price': 3.0}
{'name': 'Cup', 'price': 3.0}

Part 10: A Real Pattern - UserIn / UserOut for a Signup Endpoint#

class UserIn(BaseModel):
    username: str
    email: str
    password: str
class UserOut(BaseModel):
    username: str
    email: str
@app.post('/real-signup', response_model=UserOut, status_code=status.HTTP_201_CREATED)
def real_signup(user: UserIn):
    fake_saved_user = {'username': user.username, 'email': user.email, 'password_hash': 'not-shown'}
    return fake_saved_user
response = client.post(
    '/real-signup',
    json={'username': 'alex', 'email': 'alex@example.com', 'password': 'hunter2'}
)
print(response.status_code)
print(response.json())
201
{'username': 'alex', 'email': 'alex@example.com'}

Wrap-Up: What You Learned#

  • Returning an internal model directly leaks whatever sensitive fields it happens to hold, like a password.
  • response_model declares a separate output shape; FastAPI filters the handler's return value down to it.
  • This filtering happens regardless of how extra fields got into the returned data.
  • response_model_exclude_unset keeps only fields the caller actually set, useful for partial-update responses.
  • response_model accepts list[SomeModel], filtering every item in a returned list individually.
  • 204 No Content fits delete endpoints with no body; HTTPException with a 404 fits a failed lookup.
  • The responses parameter documents additional possible response shapes and status codes in the OpenAPI schema.
  • response_model_exclude and response_model_include offer a quick per-endpoint field shortcut without a new model.
  • A handler can return either a plain dict or an actual response_model instance; both serialize identically.
  • That wraps up response models and status codes. Next up: Data Validation Deep-Dive.

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.