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…
- CourseFastAPI deep dive
- Lesson4 of 12
- Video16 min
- FormatJupyter notebook · 10 code cells
What you'll learn
Data
No separate download needed — the notebook creates or downloads everything it uses.
📓 Full notebook
Download .ipynbFastAPI 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())
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())
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())
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())
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())
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)
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())
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())
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())
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())
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.



