Saturday, 28 March 2026

FastAPI: Return Types - Puzzle

FastAPI: return types, response_model, and HTTPException

A short note on how success (typed returns) and errors (HTTPException) fit together—using a list of Products (filtered by category) and a single Product (looked up by pid).

The idea in one paragraph

  • -> list[Products] or -> Products describe what you return when the request succeeds (usually HTTP 200).

  • raise HTTPException(status_code=404, detail="...") stops the handler and sends a 4xx response with {"detail": "..."}. A raise is not a return, so it does not clash with your success type.

  • response_model documents and serializes the 2xx body; it does not replace error handling.


How Products, list[Products] and HTTPException fit together in one small FastAPI app.


Who this is for and what puzzle we solve


If you’ve written FastAPI routes, you’ve probably seen both of these in the same function:


  • A return type like -> Products or -> list[Products]

  • A line like raise HTTPException(status_code=404, detail="...")


It can feel contradictory: “Doesn’t the function promise to return a product? Why is it allowed to raise?”


One self-contained example:  Everything below is a single friendly script

from fastapi import FastAPI, HTTPException, Query

from pydantic import BaseModel

app = FastAPI()

class Products(BaseModel):
    pid: str
    name: str
    category: str
    price: float

products: list[Products] = [
    Products(pid="p1", name="Item A", category="Electronics", price=99.0),
    Products(pid="p2", name="Item B", category="Electronics", price=49.0),
    Products(pid="p3", name="Item C", category="Toys", price=19.0),
]

@app.get("/products/category/{category_name}", response_model=list[Products])

def get_products_by_category(
    category_name: str,
    limit: int | None = Query(default=None, ge=1),
) -> list[Products]:
    want = category_name.strip().casefold()
    filtered = [p for p in products if p.category.casefold() == want]
    out = filtered if limit is None else filtered[:limit]
    if not out:
        raise HTTPException(
            status_code=404,
            detail="No products in this category",
        )
    return out

@app.get("/products/{pid}", response_model=Products)
def get_product_by_pid(pid: str) -> Products:
    found = next((p for p in products if p.pid == pid), None)
    if found is None:
        raise HTTPException(status_code=404, detail="Product not found")
    return found


What -> Products, list[Products] mean

  • For Python / your IDE
    They’re type hints: they say “on success, this function returns a Products instance” or “a list of Products.” Tools like Pyright/mypy and editors use that for checks and autocomplete.

  • For FastAPI: When you also set response_model=Products (or list[Products]) on the route, the declared response shape matches what you return on success. FastAPI uses that to:

    • Serialize the return value to JSON (and filter fields if you use response_model_include / etc.).

    • Build OpenAPI (“200 response body looks like this schema”).

  • If you only wrote -> Products and did not set response_model, recent FastAPI can still infer the response model from the annotation in many cases — but being explicit with response_model is clear and common.


Where HTTPException fits

  • raise HTTPException is not a return value. Execution leaves the function through the exception system, so there is no conflict with -> Products: the type means “when the handler finishes normally, it’s a Product.”

  • Type checkers understand that after raise, the rest of the branch doesn’t run.


Quick reference

Concept

In the example

-> list[Products] / -> Products

Documents and checks the successful JSON body.

response_model=...

Tells FastAPI how to validate/serialize that success response and document it.

HTTPException

Errors — different HTTP status and body ({"detail": ...}), not the Products schema.


Takeaway

Use the same pattern in each handler: after you load or query data, raise HTTPException when the rule says “not found,” otherwise return a list or a single model. The return type always describes the happy path only.