Skip to content

Type Hints & FastAPI Interview Questions & Answers

14 questions Updated 2026-06-20 Share:

FastAPI type hints interview questions — how Python annotations drive parameter parsing, validation, editor support and OpenAPI schema generation.

14 of 14

FastAPI uses runtime type inspection (via typing, inspect and Pydantic) to derive three things from your function signatures automatically:

  1. Where each parameter comes from (path, query, body, header).
  2. How to parse and validate the incoming value (int, str, Pydantic model).
  3. What the OpenAPI schema should look like.
@app.get("/items/{item_id}")
async def get_item(
    item_id: int,          # → path int, validated, documented as integer
    q: str | None = None,  # → optional query string, documented
    item: Item,            # → JSON body parsed into Pydantic model
):
    ...

Without type hints, FastAPI cannot infer any of this — parameters become untyped strings and no schema is generated.

Rule of thumb: in FastAPI, type annotations are not optional documentation — they are the mechanism that drives parsing, validation, and the API contract.

Give the parameter a default value of None and annotate it with str | None (Python 3.10+) or Optional[str] (Python 3.9-).

from typing import Optional

@app.get("/items")
async def list_items(
    q: str | None = None,          # optional, defaults to None
    limit: int = 10,               # optional with a non-None default
    offset: int = 0,
):
    ...

In the OpenAPI schema, parameters with defaults appear as required: false. FastAPI only marks a query param required: true when there is no default.

Rule of thumb: = None makes any parameter optional; omit the default to make it required.

Annotated[T, metadata] (from typing) attaches extra metadata to a type without changing the type itself. FastAPI reads the metadata to configure parameter behaviour — Query constraints, Body options, Depends() etc.

from typing import Annotated
from fastapi import Query, Path

@app.get("/items/{item_id}")
async def get_item(
    item_id: Annotated[int, Path(ge=1)],          # path param, must be >= 1
    q: Annotated[str | None, Query(max_length=50)] = None,
):
    ...

This keeps type information separate from validation metadata and works better with type checkers (mypy, pyright) than the old item_id: int = Path(ge=1) style.

Rule of thumb: prefer Annotated[T, Field(...)] style in new code — it separates the type from the constraint and is more explicit about what each annotation means.

Annotate the parameter with a str (or int) Enum subclass. FastAPI validates the incoming value against the enum members and documents the allowed values in OpenAPI.

from enum import Enum

class Size(str, Enum):
    small = "small"
    medium = "medium"
    large = "large"

@app.get("/items")
async def list_items(size: Size = Size.medium):
    return {"size": size.value}

Inheriting from both str and Enum ensures the value is JSON-serialisable as a string directly.

Rule of thumb: use str enums for path/query params, int enums for numeric codes; always inherit from str/int so serialisation is automatic.

Annotate with list[str] (or List[str]) and use Query() to tell FastAPI to collect multiple values for the same key.

from typing import Annotated
from fastapi import Query

@app.get("/items")
async def list_items(
    tags: Annotated[list[str], Query()] = [],
):
    return {"tags": tags}
# GET /items?tags=python&tags=fastapi → {"tags": ["python", "fastapi"]}

Without Query(), a bare list[str] annotation would be treated as a body parameter by FastAPI.

Rule of thumb: always wrap list[T] query params with Query() to signal that multiple same-key values should be collected into a list.

Annotate the parameter with Header(). FastAPI automatically converts the parameter name from snake_case to the HTTP header's hyphen-case format.

from typing import Annotated
from fastapi import Header

@app.get("/items")
async def list_items(
    x_token: Annotated[str | None, Header()] = None,
):
    # reads the "X-Token" HTTP header
    return {"token": x_token}

To read a header with underscores in the name (non-standard), pass convert_underscores=False to Header().

Rule of thumb: FastAPI converts _- automatically for headers; use convert_underscores=False only for custom non-standard header names.

Use Query() with constraint kwargs inside Annotated. These map to Pydantic field constraints under the hood.

from typing import Annotated
from fastapi import Query

@app.get("/items")
async def search(
    q: Annotated[str, Query(min_length=3, max_length=100, pattern=r"^\w+$")],
    page: Annotated[int, Query(ge=1, le=1000)] = 1,
):
    ...

Constraints are reflected in the OpenAPI schema so clients can validate before sending.

Rule of thumb: put constraint kwargs in Query()/Path()/Body() — never validate manually in the handler body for data that comes from the request.

Use Body() annotation. Without it, FastAPI treats simple types as query params.

from typing import Annotated
from fastapi import Body

@app.post("/items/{id}/tag")
async def add_tag(
    id: int,
    tag: Annotated[str, Body()],   # reads {"tag": "python"} or just "python" with embed=False
):
    ...

For a single-field body that must be wrapped in a key:

tag: Annotated[str, Body(embed=True)]   # expects {"tag": "value"}

Rule of thumb: use a Pydantic model whenever you have multiple body fields; use Body() only for a single primitive value that doesn't warrant a model.

Use Form() instead of Body(). Install python-multipart first.

from fastapi import Form

@app.post("/login")
async def login(
    username: Annotated[str, Form()],
    password: Annotated[str, Form()],
):
    ...

Note: you cannot mix JSON body and Form() fields in the same handler — the Content-Type is either application/json or application/x-www-form-urlencoded/ multipart/form-data, not both.

Rule of thumb: use Form() for HTML form submissions or OAuth2 password flows; use JSON body for REST API calls.

Use UploadFile for streaming (recommended) or bytes for small in-memory files.

from fastapi import UploadFile, File

@app.post("/upload")
async def upload(file: UploadFile):
    content = await file.read()
    return {"filename": file.filename, "size": len(content)}

UploadFile attributes: filename, content_type, file (SpooledTemporaryFile). For multiple files: files: list[UploadFile].

Always await file.read() — the file object is async. For large files, read in chunks to avoid loading the whole thing into memory:

while chunk := await file.read(8192):
    process(chunk)

Rule of thumb: prefer UploadFile over bytes — it streams large files without loading them into memory; bytes is fine only for small known-size uploads.

Set response_model_exclude_none=True in the route decorator. FastAPI calls model_dump(exclude_none=True) on the Pydantic response model before serialising.

class UserOut(BaseModel):
    id: int
    name: str
    bio: str | None = None

@app.get("/users/{id}", response_model=UserOut, response_model_exclude_none=True)
async def get_user(id: int):
    return {"id": 1, "name": "Alice"}
    # bio is None → omitted from response: {"id": 1, "name": "Alice"}

Related flags: response_model_exclude_unset=True (skip fields not explicitly set by the handler) and response_model_exclude={"internal_field"} (always strip specific fields).

Rule of thumb: use exclude_none=True for sparse models where None means "not present"; use exclude_unset=True when you want PATCH-style partial output.

Use Union[ModelA, ModelB] (or ModelA | ModelB) as the response_model. FastAPI includes both schemas in the OpenAPI spec.

from typing import Union

class Cat(BaseModel): name: str; meows: bool
class Dog(BaseModel): name: str; barks: bool

@app.get("/pet/{id}", response_model=Union[Cat, Dog])
async def get_pet(id: int) -> Cat | Dog:
    pet = await db.get_pet(id)
    return pet   # FastAPI picks the matching model for serialisation

FastAPI serialises using the first matching model from left to right in the Union. For discriminated unions, use Pydantic's discriminator field for unambiguous selection.

Rule of thumb: discriminated unions (with a type: Literal["cat"] field) are cleaner than bare Union — they document intent and avoid ambiguous serialisation.

Use Generic[T] from typing with a Pydantic BaseModel:

from typing import Generic, TypeVar
from pydantic import BaseModel

T = TypeVar("T")

class Page(BaseModel, Generic[T]):
    items: list[T]
    total: int
    page: int
    size: int

class UserOut(BaseModel):
    id: int
    name: str

@app.get("/users", response_model=Page[UserOut])
async def list_users(page: int = 1, size: int = 20):
    users, total = await db.paginate_users(page, size)
    return Page(items=users, total=total, page=page, size=size)

Pydantic v2 fully supports generic models and FastAPI generates the correct OpenAPI schema for each concrete instantiation.

Rule of thumb: define one Page[T] generic model and reuse it across all list endpoints — it keeps pagination schemas consistent and avoids duplication.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel