FastAPI: how to review AI-generated code when blocking I/O in async endpoints serializes all concurrent requests, Pydantic v2 removes .dict() and @validator at runtime, and SQLAlchemy sessions created without yield in dependencies are never closed
FastAPI is a Python web framework built on Starlette and Pydantic that uses type annotations to define request and response schemas, validates input automatically, and generates OpenAPI documentation from the annotated route signatures. It supports both synchronous and asynchronous route handlers, integrates with SQLAlchemy for database access through its dependency injection system, and is widely used as the backend for AI applications, LLM APIs, and data services. AI coding tools generate FastAPI application code that defines route handlers as async def functions, declares request and response models as Pydantic classes, and injects database sessions through Depends() functions. The generated code starts, serves requests, and returns correct responses in development testing on a single developer machine under sequential load.
The review gaps in AI-generated FastAPI code are not in the route definition syntax, the Pydantic model field declarations, or the dependency injection wiring. They are in three runtime properties that only surface under concurrent load, on the Pydantic version that FastAPI 0.100 and later actually installs, or after requests complete and session cleanup is expected to run: whether blocking I/O inside an async handler stalls the event loop, whether Pydantic v1 method calls raise AttributeError on the Pydantic v2 runtime, and whether SQLAlchemy sessions acquired through a dependency are ever released. Each gap passes in single-request sequential development testing because there is no concurrency to expose event loop blocking, no request that calls .dict() or @validator on a model instance, and no long-running server process to reveal session accumulation.
The three FastAPI review traps
1. Calling requests.get() or any other blocking library inside an async def endpoint blocks the entire event loop and serializes all concurrent requests — AI-generated async FastAPI endpoints that make HTTP calls or run blocking database queries work correctly under sequential testing but handle only one request at a time in production
FastAPI runs on an ASGI server — Uvicorn or Hypercorn — that uses a single-threaded event loop to handle concurrency. Route handlers declared as async def are coroutines: the event loop can interleave them by switching between coroutines at each await point. While one coroutine awaits a non-blocking I/O operation, the event loop runs other coroutines. This cooperative multitasking model achieves high concurrency with a single thread, but it depends on every coroutine yielding the event loop at its I/O wait points. A blocking call — requests.get(), time.sleep(), a synchronous database driver like psycopg2 or sqlite3, or any function that waits for a result without using await — does not yield the event loop. When a blocking call runs inside an async def handler, the event loop is frozen for the entire duration of that call. Every other in-flight request waits. A server that nominally handles hundreds of concurrent requests drops to handling requests serially, one at a time, for as long as each blocking call takes.
AI coding tools generate FastAPI route handlers that call external services or databases using whatever libraries appear most prominently in their training data. For HTTP calls to third-party APIs, AI tools frequently generate import requests followed by requests.get(url) inside an async def handler, because requests is the most widely documented Python HTTP library and the generated code structure looks correct. For database access, AI tools frequently generate SQLAlchemy synchronous engine calls or raw sqlite3 cursor operations inside async handlers. In development testing, a developer sends one request at a time, the blocking call completes quickly against a local or fast service, and the response comes back in milliseconds. The serial behavior is invisible because there is nothing else waiting. In production, when multiple clients send requests simultaneously, the first request to hit a blocking handler freezes the server for the duration of that call, and the developer observes high latency, request timeouts, and throughput that never scales beyond a single concurrent handler.
The fix for HTTP calls is to replace requests with httpx used with async with httpx.AsyncClient() as client: response = await client.get(url). The fix for blocking database calls is to use an async SQLAlchemy engine with an async driver — asyncpg for PostgreSQL, aiosqlite for SQLite — combined with await session.execute(). For blocking operations that cannot be made async — CPU-bound computations, legacy synchronous libraries with no async alternative — the correct pattern is to run them in a thread pool using await asyncio.get_event_loop().run_in_executor(None, blocking_function, arg), which offloads the blocking call to a worker thread and allows the event loop to handle other requests while the thread runs. The review check: for every async def route handler in AI-generated FastAPI code, verify that every I/O operation uses an await expression or is explicitly dispatched to a thread pool executor. Any synchronous library call inside an async def handler is a concurrency bottleneck that will surface as mysterious throughput degradation under concurrent load.
2. Pydantic v2 removes .dict(), .json(), @validator, and class Config — AI-generated FastAPI code using Pydantic v1 syntax raises AttributeError at runtime on the Pydantic v2 install that FastAPI 0.100+ requires, and the error only appears when the affected model method or validator is actually called
FastAPI 0.100.0, released in July 2023, switched to Pydantic v2 as its required Pydantic version. Pydantic v2 is a near-complete rewrite with significant API changes. Several widely-used Pydantic v1 patterns were removed or renamed in v2: model_instance.dict() was replaced by model_instance.model_dump(), model_instance.json() was replaced by model_instance.model_json(), the @validator decorator was replaced by @field_validator with a changed signature, the class Config inner class was replaced by model_config = ConfigDict(...), and schema() was replaced by model_json_schema(). Pydantic v2 ships a compatibility shim under pydantic.v1 for code that imports directly from that submodule, but code that calls v1 methods on v2 model instances — such as calling .dict() on an instance of a class that inherits from pydantic.BaseModel — raises AttributeError: 'YourModel' object has no attribute 'dict' at runtime.
AI coding tools generate Pydantic model code using v1 syntax because the v1 patterns dominated training data for years and remain the most common pattern in code examples, Stack Overflow answers, and tutorials online. The generated code defines models with class Config: orm_mode = True, adds field validation with @validator('field_name'), and serializes model instances with instance.dict(). This code passes static type checking because mypy and pyright type stubs for Pydantic v2 do not flag calls to removed methods as errors — the stubs focus on the new API and do not annotate removed methods as absent. The error is invisible until the specific code path that calls the removed method is exercised at runtime. A FastAPI application may start, serve many requests, and appear fully functional until a request triggers the serialization path, the validation path, or the schema export path that calls the v1 method. Only that specific request raises the error; all other request paths that do not call the v1 method continue working. Development testing that exercises only the happy path for common requests may never call .dict() on the affected model, and the bug sits undetected until a client exercises the affected endpoint.
The Pydantic v2 migration is additionally subtle because many projects specify pydantic>=1.0 or pydantic>=1.9 in their requirements, intending to allow v1 but not constraining against v2. When FastAPI 0.100+ is installed alongside such a requirements specification, pip resolves to Pydantic v2, and all v1 method calls in the codebase become latent runtime errors. The review check: for every AI-generated Pydantic model in a FastAPI application, verify that there are no calls to .dict(), .json(), .schema(), no @validator decorators, and no class Config inner classes. The v2 equivalents are .model_dump(), .model_json(), .model_json_schema(), @field_validator (with @classmethod and the new info parameter signature), and model_config = ConfigDict(...). Also check that orm_mode = True has been replaced by from_attributes = True inside ConfigDict. AI-generated code that passes static analysis but calls v1 methods on v2 model instances is a runtime error deferred to the first request that exercises the affected code path.
3. SQLAlchemy sessions created inside FastAPI dependency functions without a yield statement are never closed after the request completes — AI-generated dependency code that creates sessions with a plain return leaks connections and exhausts the database connection pool under sustained load
FastAPI’s dependency injection system supports two patterns for dependency functions: a plain function that returns a value, and a generator function that yields a value and then runs cleanup code after the response is sent. The generator pattern is the standard way to implement resource acquisition and release — the code before yield acquires the resource, the yielded value is injected into the route handler, and the code after yield runs in a finally block after the request completes, whether it succeeded or raised an exception. For SQLAlchemy sessions, the standard pattern creates a session from the engine’s sessionmaker, yields it, and closes it in the post-yield cleanup: def get_db(): db = SessionLocal(); try: yield db; finally: db.close(). This ensures the session is returned to the connection pool after every request, regardless of whether the handler succeeded or raised an HTTP exception.
AI coding tools frequently generate FastAPI database dependency functions that use return instead of yield: def get_db(): return SessionLocal(). This code creates a new SQLAlchemy session for each request and injects it into the route handler correctly. The handler can use the session to run queries, commit transactions, and return results. In development testing under sequential low load, the function appears correct — each request gets a session and the query results are right. What never happens is cleanup: because the dependency uses return and not yield, FastAPI has no post-response hook to run. The session is injected into the handler as a local variable. After the handler function returns, Python’s garbage collector is the only mechanism that will eventually close the session. In CPython, the garbage collector typically closes the session promptly when the handler’s local variables are freed. But under concurrent load, many sessions may be open simultaneously — one per concurrent request — and SQLAlchemy’s connection pool has a fixed size. When the number of concurrent requests exceeds the pool size, new requests block waiting for a connection to be returned to the pool. Because sessions are not explicitly closed at request end, they hold their connections longer than necessary, and under sustained concurrent load the pool is exhausted. New requests that arrive when the pool is full receive a sqlalchemy.exc.TimeoutError after waiting for pool_timeout seconds.
The failure is invisible in development because single-request sequential testing never accumulates more than one open session at a time. Even load testing with a small number of concurrent requests may not trigger the pool exhaustion if the requests are short enough that sessions are garbage-collected before the pool fills. Pool exhaustion typically surfaces in production under sustained load, with a characteristic signature: requests succeed until the service has been running for some time under load, then a subset of requests begin timing out with connection pool errors while others succeed, because the pool is saturated by sessions that were created but never explicitly closed. The intermittent nature of the failure and its delayed onset after sustained load make it one of the harder FastAPI production bugs to diagnose without knowing to look at session lifecycle.
The review check: for every AI-generated FastAPI dependency function that creates a SQLAlchemy session or any other resource that must be explicitly released, verify that the function uses yield instead of return, and that the post-yield cleanup block calls db.close() or the equivalent release method inside a finally clause. A dependency function with return SessionLocal() is a connection leak that will exhaust the pool under sustained concurrent load. The generator dependency form def get_db(): db = SessionLocal(); try: yield db; finally: db.close() guarantees cleanup on every request path including exception paths. For async SQLAlchemy, the equivalent uses async def get_db() with async with AsyncSession(engine) as session: yield session. AI-generated code that creates a database session with a plain return in a dependency function is a resource leak that will manifest as connection pool exhaustion in any production service that receives sustained concurrent traffic.
Reviewing FastAPI code against event loop blocking, Pydantic version compatibility, and session cleanup guarantees, not against single-request development testing
FastAPI is a framework whose critical behaviors — async concurrency, Pydantic version compatibility, and dependency lifecycle management — are all invisible in the single-request sequential testing environment where AI coding tools validate their output. A developer who starts a FastAPI server, sends one request at a time, and reads the JSON response sees correct behavior regardless of whether the handler blocks the event loop, regardless of whether the model’s .dict() call would raise AttributeError if triggered, and regardless of whether the database session will be closed after the request completes. The three review surfaces in AI-generated FastAPI code represent gaps between the single-request development environment and the production environment where multiple clients send concurrent requests, the Pydantic v2 runtime executes every code path that accesses model methods, and sessions accumulate across thousands of requests.
A practical review approach for AI-generated FastAPI code: when you see any synchronous I/O library call — requests.get(), urllib.request.urlopen(), psycopg2.connect(), sqlite3.connect(), a synchronous SQLAlchemy session.execute() — inside an async def route handler, verify that the call is awaited or dispatched to a thread pool executor, because any blocking call inside an async handler freezes the event loop and serializes all concurrent requests for the duration of that call. When you see Pydantic model code that calls .dict(), .json(), uses @validator, or defines a class Config inner class, verify that the installed Pydantic version is v1, because FastAPI 0.100+ requires Pydantic v2 where these patterns raise AttributeError at runtime and the error is deferred to the first request that exercises the affected code path. When you see a FastAPI dependency function that creates a SQLAlchemy session and uses return instead of yield, verify whether the session will be closed after every request, because a plain return gives FastAPI no post-response cleanup hook, sessions accumulate without being returned to the pool, and the connection pool is exhausted under sustained concurrent load. These three checks address the async blocking gap, the Pydantic version compatibility gap, and the session lifecycle gap that appear consistently in AI-generated FastAPI code and are invisible in the sequential single-request development testing environment.
Related reading: Pydantic AI on reviewing AI-generated agent code where Pydantic model validation, dependency injection scope, and structured output handling share the single-request testing blind spot. LangChain on reviewing AI-generated chain code where async execution, streaming, and callback handler patterns produce similar invisible production failures. Prisma on reviewing AI-generated database access code where connection pooling, transaction scope, and migration drift create comparable gaps between development testing and production behavior. How to review AI-generated code for the general checklist that applies when AI generates web framework integration code.
FastAPI generated the routes. ZenCode checks whether the async handlers block, the Pydantic version matches, and the sessions close.
ZenCode surfaces one concrete review question before you commit — including when AI-generated FastAPI code uses blocking requests inside async endpoints, calls .dict() on a Pydantic v2 model, or creates SQLAlchemy sessions with return instead of yield so they never close.
Try ZenCode free