Python & FastAPI Vibe Code Cleanup
Your AI mixed sync and async, skipped dependency injection, and left SQLAlchemy sessions open. Time to fix it.
At Variant Systems, we pair the right technology with the right approach to ship products that work.
Why this combination
- AI mixes sync and async code, blocking the event loop without warning
- Pydantic models are too permissive with Optional fields and missing validators
- SQLAlchemy sessions leak because AI doesn't use dependency injection properly
- No separation between route handlers, business logic, and data access
Async Traps Copilot Leaves Behind
FastAPI is async-first. AI doesn’t respect that. It generates def endpoints instead of async def. It calls synchronous libraries inside async routes. It imports requests instead of httpx. It uses time.sleep() in async contexts. Each of these blocks the event loop. Under light load, you don’t notice. Under production traffic, your API stops responding because a single sync call freezes the entire worker.
Pydantic models from AI are too loose. Every field is Optional. Validators are missing. AI uses dict or Any where it should use a specific model. The whole point of FastAPI is type-safe request validation - AI throws that advantage away by making everything nullable and unvalidated.
SQLAlchemy session management is where things get dangerous. AI creates sessions inline with Session() calls. It doesn’t close them in error paths. It shares sessions across requests. It doesn’t use FastAPI’s dependency injection system, which handles session lifecycle automatically. Under load, you run out of database connections because sessions accumulate and never close.
The architecture is flat. Route handlers contain business logic, database queries, external API calls, and response formatting in one function. A single endpoint handler is 80 lines long. There’s no service layer, no repository pattern, no separation of concerns. Testing means spinning up the entire application for every test.
Untangling the Event Loop and Hardening Pydantic
We audit every route handler for async correctness first. Every def endpoint that should be async def gets converted. Every sync library call gets replaced with its async equivalent or moved to run_in_executor. We verify with asyncio debug mode enabled, which logs every blocking call that exceeds the threshold. Nothing hides.
Pydantic models get hardened. We replace Optional fields with required fields where the API contract demands it. We add field validators for business rules - email format, string length, numeric ranges. We create separate request and response models so internal fields never leak to the client. model_config = ConfigDict(strict=True) ensures no silent type coercion.
SQLAlchemy sessions move to FastAPI dependencies. A single get_db dependency creates and closes the session. Every route that needs database access declares the dependency. Async sessions use async_sessionmaker with AsyncSession. The connection pool is configured with proper limits, overflow, and recycle settings. Sessions close on every request, success or failure.
We introduce layers. Route handlers parse the request and return the response. Service functions contain business logic. Repository functions handle database operations. Each layer has its own types and its own tests. A route handler is 10 lines, not 80.
From 50 Concurrent Requests to 500
Before: An API that handles 50 concurrent requests before response times spike from 100ms to 8 seconds. Database connections exhaust after an hour of moderate traffic. Pydantic models that accept null for fields your database requires to be non-null. Route handlers that are untestable without mocking everything.
After: The same API handles 500 concurrent requests with consistent 50ms response times. Database connections stay within pool limits. Pydantic rejects invalid data at the boundary before it reaches your business logic. Each layer has focused unit tests that run in milliseconds.
The monitoring tells the story. Before cleanup, you see periodic connection pool exhaustion errors and 502s during traffic spikes. After, the connection pool stays healthy and response times are flat regardless of load.
Mypy, Ruff, and Strict Schemas as Your Safety Net
Mypy strict mode runs in CI. No Any types. No untyped function definitions. No implicit Optional. AI-generated code that uses dict instead of a proper Pydantic model fails the type check before it reaches code review.
Ruff replaces Flake8 and isort with a single, fast linter. We configure rules that catch common AI patterns: unused imports, bare except clauses, mutable default arguments, and f-strings with no expressions. The linter runs in pre-commit so issues are caught immediately.
We add pytest-asyncio with strict mode for the test suite. Every async test must be explicitly marked. Sync tests that accidentally pass because of implicit event loop creation fail instead. Test factories use factory_boy with proper database session scoping so tests don’t leak data between runs.
API documentation stays in sync with the code because FastAPI generates it from your Pydantic models. When the models are strict and well-typed, the auto-generated OpenAPI spec is accurate. Clients trust the spec because the spec is the code. No documentation drift.
What you get
Ideal for
- FastAPI apps built with AI that block the event loop under load
- Products with intermittent database connection errors from session leaks
- Teams seeing type-related bugs that Python's dynamic typing hides
- Founders who need their API to handle real production traffic reliably