Flask
Still widely deployed, so still asked about — usually as “why would you pick this over FastAPI”, and as a way to check you understand what a framework does rather than which one is fashionable.
The model
A microframework: routing, request/response, templating via Jinja2, and nothing else. Everything additional is an extension you choose.
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.get("/users/<int:user_id>")
def get_user(user_id: int):
user = db.session.get(User, user_id)
if user is None:
return jsonify(error="not found"), 404
return jsonify(user.to_dict())
Compare FastAPI, where the type annotation is the validation and the OpenAPI schema. In Flask you write validation, serialisation and documentation yourself, or add extensions that do.
The app factory
The pattern to know, because the naive app = Flask(__name__) at module scope makes testing and configuration awkward.
def create_app(config: str = "config.Production") -> Flask:
app = Flask(__name__)
app.config.from_object(config)
db.init_app(app) # extensions bound here, not at import
migrate.init_app(app, db)
from .api import bp as api_bp
app.register_blueprint(api_bp, url_prefix="/api")
return app
Why it matters: you can create a differently-configured app per test, extensions aren’t bound at import time, and circular imports mostly disappear. Blueprints are the module unit — a group of routes with its own prefix, templates and error handlers.
Application and request context
Flask’s most distinctive concept, and the source of its most confusing errors.
from flask import g, current_app, request
request, g and current_app are context-local proxies — they look like globals but resolve to whatever is bound to the current request or app context. g is per-request scratch space; current_app is the active app.
The classic error, Working outside of application context, means you touched one of these outside a request — in a CLI command, a background thread, or at import time. The fix:
with app.app_context():
do_something_using_current_app()
This design predates async and is why Flask’s async story is awkward: context-locals were built on thread-locals.
Flask vs FastAPI
| Flask | FastAPI | |
|---|---|---|
| Interface | WSGI (sync) | ASGI (async-native) |
| Validation | manual, or an extension | Pydantic, built in |
| OpenAPI docs | extension | automatic |
| Async support | partial (3.x), still WSGI underneath | native |
| Type hints | optional | structural |
| Ecosystem | huge, mature | large, newer |
| Server-rendered HTML | excellent — Jinja2 | possible, not the focus |
For a new JSON API in 2026, FastAPI is the default. Type-driven validation, free OpenAPI docs and native async are hard to argue against. See ../fastapi/.
Flask still wins for server-rendered applications — Jinja2 templating, forms, sessions and a mature extension ecosystem for admin, auth and login. And a large amount of production Python runs on it, which is why the question comes up.
Flask 3.x added async def view support, but it runs the coroutine in a worker thread because Flask is still WSGI. You get async syntax without async concurrency, which is worth understanding rather than assuming it’s equivalent. See ../00_wsgi_vs_asgi.md.
The extension ecosystem
| Need | Extension |
|---|---|
| ORM | Flask-SQLAlchemy |
| Migrations | Flask-Migrate (Alembic) |
| Auth sessions | Flask-Login |
| Forms and CSRF | Flask-WTF |
| Serialisation/validation | Marshmallow, or Pydantic directly |
| Admin | Flask-Admin |
The trade-off in one line: Flask gives you choice, and choice is work. FastAPI makes more decisions for you, which is why it’s faster to start and more opinionated to live with.
Deployment
gunicorn -w 4 -k gthread --threads 4 "app:create_app()"
WSGI, so a synchronous worker model: processes for parallelism, threads for I/O concurrency. Worker count around 2 × cores + 1 as a starting point, then measure. See ../../12_protocols/nginx/07_gunicorn_python_deploy.md.
Interview angle
- “Flask or FastAPI for a new API?” — FastAPI: Pydantic validation from type hints, automatic OpenAPI, native async on ASGI. Flask remains a good choice for server-rendered applications where Jinja2 and its extension ecosystem are the point.
- “What’s the app factory pattern and why use it?” — a function that builds and configures the app, so extensions bind inside it rather than at import. It makes per-test configuration possible and removes most circular-import problems.
- “What does ‘working outside of application context’ mean?” — you accessed a context-local proxy like
current_apporgoutside a request or app context, typically in a CLI command, a thread, or at import time. Wrap the code inwith app.app_context():. - “Flask 3 supports
async def. Is it the same as FastAPI?” — no. Flask is still WSGI, so a coroutine runs in a worker thread. You get the syntax without the single-threaded concurrency model, and none of the throughput benefit for I/O-bound work. - “What’s a blueprint?” — a group of routes with their own prefix, templates and error handlers, registered onto the app. It’s Flask’s module boundary and the reason large Flask apps stay navigable.