TL;DR: FastAPI interviews in 2026 test async versus sync endpoints, the dependency system, Pydantic v2 models and how work leaves the request. Candidates should know that FastAPI no longer supports Pydantic v1 at all and needs Python 3.10 or later.
FastAPI 0.128.0, released on December 27, 2025, removed the last of its Pydantic v1 support. Every @validator and class Config in an old codebase now has to be Pydantic v2 code.
Since then the framework has added strict JSON content-type checks, faster JSON output and streaming with yield.
- 1Current versions as of September 2026: FastAPI 0.141.1, Pydantic 2.13.5, Starlette 1.7.0 and Uvicorn 0.53.0.
- 2Since 0.130, FastAPI serializes JSON through Pydantic's Rust core when a route declares a response model or return type, which the release notes put at 2x or more faster.
- 3Starlette, the toolkit under FastAPI, reached 1.0 on March 22, 2026, after nearly eight years.
- 4The FastAPI docs now use PyJWT and pwdlib with Argon2 in the JWT tutorial.
FastAPI Core
1. How does FastAPI use Python type hints?
FastAPI reads the type hints on a path function to do three jobs at once: parse and validate the request, convert the response, and generate the OpenAPI schema. Take the annotation item_id: int.
FastAPI reads the value from the path and converts it to an integer. It rejects the request with a 422 if that fails, and documents the parameter in /docs.
The validation itself is done by Pydantic. FastAPI's own code decides where each value comes from and wires everything to Starlette, which handles HTTP.
2. When should an endpoint be async def and when plain def?
Use async def when every slow call inside it can be awaited. Use plain def when it calls blocking libraries. The FastAPI async docs explain that a plain def path function runs in an external threadpool, so it does not block the server.

The dangerous case is a blocking call inside async def, such as requests.get() or time.sleep(). It runs on the event loop and freezes every other request on that worker until it returns. The same rules apply to dependencies.
def routes share one pool of threads. Many slow sync routes can exhaust it, and then requests queue even though CPU use looks low.3. How does FastAPI decide whether a parameter comes from the path, query or body?
By its name and type. A parameter whose name appears in the path template is a path parameter. A parameter with a simple type, such as int or str, is a query parameter. A parameter typed as a Pydantic model is read from the JSON body.
You override the defaults with Query(), Header(), Cookie(), Body() and Form(). Since 0.115.0, a Pydantic model can also group query, header or cookie parameters, for example a FilterParams model used as Annotated[FilterParams, Query()].
4. Why use Annotated for parameters and dependencies?
Because it keeps the real default value in the normal place and puts FastAPI's metadata beside the type. The FastAPI docs use this style throughout.
from typing import Annotated
from fastapi import Depends, Query
async def list_items(
user: Annotated[User, Depends(get_current_user)],
q: Annotated[str | None, Query(max_length=50)] = None,
): ...
The practical gain is reuse. You can define CurrentUser = Annotated[User, Depends(get_current_user)] once and use user: CurrentUser in every route.
5. What does a response model do beyond documentation?
It filters and validates what leaves the API. Say a route declares response_model=UserOut or a return type of UserOut. FastAPI drops any field not in UserOut, even if the function returned a database object with a hashed_password attribute.
That makes separate input and output models a security feature, not just tidiness. Returning a Response object directly skips all of this, so a raw JSONResponse is neither filtered nor documented.
6. How do errors turn into HTTP responses?
Raise HTTPException(status_code=404, detail="...") for expected errors, and FastAPI returns it as JSON. Invalid input raises RequestValidationError, which becomes a 422 with the location and reason of each problem.
Register handlers with @app.exception_handler(SomeError) to map domain exceptions, such as InsufficientFunds, to status codes in one place. Services can then raise their own errors and stay free of HTTP details.
Unhandled exceptions become a 500 with no stack trace sent to the client.
7. How do you structure a large FastAPI app?
Split routes into APIRouter instances by domain, such as users and orders, and include them in the app with a prefix and tags. Keep business logic in plain functions or service classes that the routes call.
Keep database access behind dependencies, so tests can replace it.
Router-level dependencies=[Depends(verify_token)] applies a check to every route in that router. Since 0.137.0, routers are kept as real objects instead of being copied into the app, so routes added after include_router() still register.
Pydantic v2 Models
8. How do you write custom validation in Pydantic v2?
Use @field_validator for one field and @model_validator for rules across fields. The v1 decorators @validator and @root_validator are deprecated in Pydantic v2, per the Pydantic migration guide.
class Booking(BaseModel):
start: date
end: date
@field_validator("start")
@classmethod
def not_in_past(cls, v: date) -> date:
if v < date.today():
raise ValueError("start is in the past")
return v
@model_validator(mode="after")
def end_after_start(self) -> "Booking":
if self.end <= self.start:
raise ValueError("end must be after start")
return self
Simple constraints need no validator at all: Field(gt=0, max_length=100) covers most of them and shows up in the OpenAPI schema.
9. How do you implement a PATCH endpoint correctly?
Make every field optional in the update model, then apply only the fields the client actually sent with model_dump(exclude_unset=True).
@app.patch("/items/{item_id}")
async def update_item(item_id: int, patch: ItemUpdate, db: DB):
item = await get_item_or_404(db, item_id)
for key, value in patch.model_dump(exclude_unset=True).items():
setattr(item, key, value)
await db.commit()
return item
Without exclude_unset, an omitted field arrives as its default, often None, and overwrites real data. Candidates coming from Pydantic v1 may still write .dict(), which v2 replaced with model_dump().
10. How should a FastAPI app load settings?
With a BaseSettings class from the separate pydantic-settings package. It reads environment variables and .env files, validates them at startup and gives typed access.
Provide it through a dependency wrapped in @lru_cache, so the settings are read once and tests can override them. In v2, configuration moved from an inner class Config to model_config = SettingsConfigDict(env_file=".env").
Dependencies and Auth
11. How does Depends() work, and are dependencies cached?
Depends() tells FastAPI to call a function, class or other callable first and pass its result in. Dependencies can have their own dependencies, forming a tree that FastAPI resolves for each request.
Within one request, each dependency runs once and its result is reused by every part of the tree that asks for it. Pass use_cache=False when you need a fresh call each time.
Nothing is cached between requests, which is why expensive setup belongs in the lifespan instead.
12. How do dependencies with yield manage a database session?
Code before yield runs before the path function; code after it runs as cleanup. That makes it the standard way to open and close a session per request.
async def get_db():
async with SessionLocal() as session:
yield session
DB = Annotated[AsyncSession, Depends(get_db)]
By default the cleanup runs after the response is sent. Exceptions raised in the route reach the dependency, so a try block around yield can roll back, but it should re-raise. See dependencies with yield.
13. How do you add JWT authentication?
Use OAuth2PasswordBearer to read the bearer token, and a get_current_user dependency that decodes and verifies it. The current JWT tutorial uses PyJWT for tokens and pwdlib with Argon2 for password hashes.
Authorization is a second dependency that uses the user, such as require_role("admin"). A strong candidate mentions short token lifetimes and refresh tokens stored server-side so they can be revoked.
They also keep secrets out of the token payload, which is only encoded, not encrypted. Since 0.122.0, the security classes return 401 rather than 403 when credentials are missing.
14. How do you test an endpoint that depends on a database or auth?
Replace the dependency with app.dependency_overrides and call the app with TestClient.
def fake_user():
return User(id=1, roles=["admin"])
app.dependency_overrides[get_current_user] = fake_user
client = TestClient(app)
assert client.get("/admin/stats").status_code == 200
app.dependency_overrides = {}
Reset the overrides after each test, usually in a fixture, or they leak into other tests. For async tests that share an event loop with async database code, use httpx.AsyncClient with an ASGI transport instead of TestClient.
Async and Data
15. A blocking call is stuck inside async def. How do you fix it?
Swap it for an async library if one exists, or move the call to a thread. httpx.AsyncClient replaces requests; asyncpg or SQLAlchemy's async engine replaces a sync driver.
When no async version exists, run the call with await run_in_threadpool(func, *args) from Starlette, or anyio.to_thread.run_sync(). The quick alternative is to change the route to plain def.
The symptom to recognise in an incident is latency rising on every endpoint of a worker, not just the slow one.
16. How do you use SQLAlchemy 2.0 with async FastAPI?
Create an engine with create_async_engine() and an async_sessionmaker, and give each request its own AsyncSession through a yield dependency. Never share one session across requests; a session is not safe for concurrent use.
The trap candidates hit is lazy loading. Accessing a relationship that was not loaded triggers I/O, which fails in async code.
Load relationships up front with selectinload(), as the SQLAlchemy asyncio docs describe, or set expire_on_commit=False so attributes stay readable after commit.
17. When is BackgroundTasks enough, and when do you need a queue?
BackgroundTasks runs a function after the response is sent, in the same process. It suits small jobs such as sending one email. The background tasks docs point heavier work to bigger tools such as Celery.

The deciding facts: a background task is lost if the worker restarts, has no retries, and uses the same CPU as your requests.
If the job must happen, needs retries or takes more than a few seconds, it belongs in a queue with its own workers.
18. Where do startup and shutdown code go?
In a lifespan function passed to FastAPI(lifespan=...). Code before yield runs once at startup and code after it runs at shutdown.
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.http = httpx.AsyncClient()
yield
await app.state.http.aclose()
app = FastAPI(lifespan=lifespan)
Use it for connection pools, HTTP clients and ML models. The older @app.on_event("startup") handlers are marked deprecated in the lifespan events docs.
19. How would you serve an ML model with FastAPI?
Load the model once in the lifespan, not per request, and keep inference off the event loop. Inference is CPU or GPU work, so calling it directly in async def blocks every other request on that worker.
For light models, a plain def route or a thread is enough, since many numeric libraries release the GIL. For heavy models, run inference in a separate service or worker pool and let FastAPI handle HTTP, validation and batching.
Scale by processes or replicas, and remember that each worker process loads its own copy of the model into memory.
20. What changes when WebSockets run on several workers?
Each connection lives in one worker process, so an in-memory list of connections only sees that worker's clients. A broadcast from worker A never reaches a client connected to worker B.
The fix is a shared channel, such as Redis pub/sub, that every worker subscribes to and forwards to its own clients. The load balancer must also support WebSocket upgrades and long-lived connections.
For one-way updates, Server-Sent Events are simpler and now built in.
Production
21. How do you run FastAPI in production?
Run it with an ASGI server: fastapi run or uvicorn, both of which accept --workers to start several processes. The server workers docs show both commands.
The same page notes that on Kubernetes you will usually run a single Uvicorn process per container and let the cluster handle replication. Add a health endpoint and graceful shutdown.
Behind a load balancer, set Uvicorn's --forwarded-allow-ips so client IPs and HTTPS are read from the proxy headers.
22. Middleware or dependency: which do you use for a cross-cutting concern?
Use middleware for things every request needs regardless of route, such as request IDs, timing, CORS and GZip. Use a dependency when the logic needs route context, typed values or should appear in OpenAPI, such as authentication.
Middleware sees raw requests and responses, runs even for 404s, and cannot use Depends(). Dependencies can be applied per route, per router or app-wide, and are easy to override in tests.
CORS in particular must be CORSMiddleware, because preflight OPTIONS requests never reach a route.
23. How do you find why an endpoint is slow?
Measure where the time goes before changing code. Add tracing, for example OpenTelemetry's FastAPI instrumentation, to split a request into database time, outbound HTTP and Python time.
- All endpoints slow at once: something blocks the event loop. Look for sync calls inside
async def. - One endpoint slow: log its SQL and look for N+1 queries and missing indexes.
- Slow under load only: check connection pool size and threadpool saturation from many plain
defroutes.
py-spy can sample a running worker without restarting it, which shows exactly which function holds the CPU.
24. How do you version a FastAPI API?
Most teams put the version in the path, with one APIRouter per version mounted at /v1 and /v2. Both versions can share services and differ only in their request and response models.
Add a new version only for breaking changes; adding an optional field is not one. Mark old routes with deprecated=True so they show as deprecated in /docs, and publish a removal date.
What Changed Recently
scope for dependencies with yield25. What happened to Pydantic v1 and older Python versions?
Both are gone. FastAPI 0.126.0 dropped Pydantic v1, and 0.128.0 removed the temporary support for pydantic.v1 models inside Pydantic v2. FastAPI 0.129.0 dropped Python 3.9, so the package now requires Python 3.10 or later.
For an interview, this separates candidates who maintain current code from those who learned from older tutorials. Signs of v1 habits are @validator, class Config, .dict() and orm_mode, which v2 replaced with from_attributes.
26. Why can a JSON request suddenly fail after upgrading to 0.132?
Because FastAPI now rejects JSON bodies that lack a valid JSON Content-Type header. The 0.132.0 release made this the default, and clients that send no header, or text/plain, now fail.
The reason is security. The strict Content-Type docs describe a CSRF attack on apps without authentication, such as a local AI agent on localhost.
A malicious site can post a body with no content type, and the browser sends it without a CORS preflight. If you control old clients that cannot be fixed, set strict_content_type=False.
27. How did JSON response performance change in 0.130?
When a route declares a response model or a return type, FastAPI now serializes the JSON with Pydantic's Rust code directly. The 0.130.0 release notes describe a 2x or greater speed-up for JSON responses.
The next release, 0.131.0, deprecated ORJSONResponse and UJSONResponse. The practical advice for candidates flips: declare a return type instead of reaching for a faster JSON library.
Routes that return a JSONResponse directly do not get this path.
- Data converted with
jsonable_encoder - Then encoded by the standard
jsonmodule ORJSONResponsefor speed
- Response model or return type declared
- Pydantic serializes in Rust
ORJSONResponsedeprecated in 0.131
28. How do you stream data from FastAPI now?
Write the path function as a generator and yield items. Since 0.134.0, FastAPI streams yielded items as JSON Lines, with the application/jsonl content type.
Since 0.135.0, setting response_class=EventSourceResponse sends them as Server-Sent Events.
from collections.abc import AsyncIterable
from fastapi.sse import EventSourceResponse
@app.get("/tokens", response_class=EventSourceResponse)
async def tokens() -> AsyncIterable[Token]:
async for t in model.stream():
yield t
Declaring the item type validates and documents each event. The SSE docs list AI chat streaming and live notifications as typical uses.
29. What does the scope option on Depends() control?
It controls when the cleanup code of a dependency with yield runs. FastAPI 0.121.0 added Depends(scope="function"), which closes the dependency when the path function returns, before the response is sent.
The default, scope="request", closes it after the response.
Use "function" to release a scarce resource, such as a database connection, as soon as the handler is done. Keep "request" when a streaming response still needs the resource while it sends.
A request-scoped dependency cannot depend on a function-scoped one, because its cleanup would run after the other is already closed.
30. What changed about routers in 0.137.0?
Routers are now kept as real objects inside the app instead of having their routes copied in. Per the 0.137.0 release, routes added to a router after it has been included now work.
A router can even be included before its routes are defined.
The breaking part: router.routes is no longer a flat list of APIRoute objects but a tree. Code that walked it, for example to add tags or build custom docs, needs updating, and 0.137.2 added iter_route_contexts() for those uses.
The release notes also list per-router dependencies, exception handlers and middleware as planned next steps.
Signs of a Strong Answer
- They can say what happens to other requests when
requests.get()runs insideasync def, and how they would spot it in production. - They use separate input and output models and know the response model strips fields like password hashes.
- They write Pydantic v2 code by habit:
field_validator,model_dump(),model_config. - They use
exclude_unsetfor PATCH without being prompted. - They move work that must not be lost into a real queue, not
BackgroundTasks. - They know at least one change from the last year, such as strict Content-Type checks or the Rust JSON path, and what it meant for their code.
Hiring FastAPI Developers
Good FastAPI developers are good Python developers first, with solid async and database habits. Second Talent matches companies with pre-vetted Python developers, including FastAPI specialists, screened with questions like these.
Tell us the stack and we send a shortlist within 24 hours. Start hiring, or see our Python and Django interview guides.






