Python's web development landscape has evolved significantly, with Django leading the full-stack space and microframeworks gaining traction for their flexibility.
Flask, introduced in 2010, became popular for its minimalist yet extensible design, making it ideal for everything from simple APIs to complex web services. FastAPI, launched in 2018, brought modern Python features and high performance, quickly becoming one of the fastest-growing frameworks.
This article compares Flask and FastAPI, exploring their architecture and development experience to help you choose the right fit for your project.
Flask
Flask embraces a "micro" philosophy, offering just enough structure to build web applications without unnecessary constraints.
It prioritizes clarity, requiring you to make intentional design choices rather than relying on hidden complexity.
Instead of packing in features, Flask keeps the core minimal and extends functionality through plugins. This lightweight approach makes it easy to start while offering the flexibility to scale for more complex applications.
FastAPI
FastAPI takes a structured approach, using Python’s type hints for validation, documentation, and a smoother development experience.
It runs on Starlette and Pydantic, making async support and high performance the default rather than an afterthought.
OpenAPI and JSON Schema integration ensure smooth compatibility and automatic documentation generation, reducing the need for manual setup.
Framework architecture and components
Both frameworks solve similar problems but differ significantly in their architectural approaches.
Flask is built on two main components:
- Werkzeug - A WSGI utility library that handles HTTP requests and responses
- Jinja2 - A template engine for rendering HTML
Flask follows the traditional WSGI (Web Server Gateway Interface) protocol, which defines how Python web applications communicate with web servers. This synchronous design means each request occupies a worker until completion.
The central component in Flask is the app
object, which serves as both the configuration hub and the routing mechanism:
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/items/<int:item_id>')
def get_item(item_id):
# Access item from database
item = {"id": item_id, "name": "Example Item"}
return jsonify(item)
if __name__ == '__main__':
app.run(debug=True)
Flask uses a context-based system with request
and g
objects to maintain state during request processing. This approach is simple but can be challenging to understand initially, especially in larger applications.
Conversely, FastAPI builds on more modern components:
- Starlette - A lightweight ASGI framework providing routing and middleware
- Pydantic - A data validation library using Python type annotations
- Uvicorn/Hypercorn - ASGI servers for handling HTTP requests
FastAPI uses ASGI (Asynchronous Server Gateway Interface) to handle requests asynchronously and natively support WebSockets and other protocols. This design enables higher concurrency and better performance for I/O-bound operations.
The foundation of a FastAPI application looks similar to Flask but incorporates type hints and async capabilities:
from fastapi import FastAPI, Path
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str = None
price: float
tax: float = None
@app.get('/items/{item_id}')
async def get_item(
item_id: int = Path(..., gt=0, description="The ID of the item to retrieve")
):
return {"id": item_id, "name": "Example Item"}
FastAPI's architecture is more opinionated, encouraging a specific development style around type annotations and data models.
Request handling and routing
Both frameworks use decorators for routing, but their approaches to request handling differ significantly.
Flask uses route decorators that specify URL patterns and HTTP methods:
@app.route('/users', methods=['GET'])
def get_users():
return jsonify([
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"}
])
@app.route('/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
# Route with path parameter
return jsonify({"id": user_id, "name": "Example User"})
Request data is accessed through the global request
object:
@app.route('/users', methods=['POST'])
def create_user():
data = request.json
# Validate data manually
if 'name' not in data:
return jsonify({"error": "Name is required"}), 400
# Process data
return jsonify({"id": 3, "name": data['name']}), 201
Flask's approach is straightforward but requires manual validation and type conversion. This can lead to verbose code with repetitive validation logic for complex APIs.
Meanwhile, FastAPI uses operation decorators that include both the path and HTTP method:
@app.get('/users')
async def get_users():
return [
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"}
]
@app.get('/users/{user_id}')
async def get_user(user_id: int):
# Path parameter with automatic type conversion
return {"id": user_id, "name": "Example User"}
For request bodies, FastAPI uses Pydantic models with automatic validation:
class UserCreate(BaseModel):
name: str
email: str
age: int = Field(None, ge=18, description="User must be an adult")
@app.post('/users', status_code=201)
async def create_user(user: UserCreate):
# Data is already validated and converted
return {"id": 3, **user.dict()}
FastAPI's approach reduces boilerplate and ensures consistent validation. Type annotations serve triple duty: validation, documentation, and editor support.
Performance and efficiency
The performance profile of these frameworks differs dramatically due to their architectural foundations.
Flask uses a synchronous execution model built on WSGI. Each request is processed entirely before moving to the next, potentially leading to bottlenecks with I/O-bound operations:
@app.route('/data')
def get_external_data():
# This blocks the entire worker while waiting
response = requests.get("https://api.example.com/data")
data = response.json()
# Additional processing
processed_data = process_data(data)
return jsonify(processed_data)
For high-concurrency scenarios, Flask typically scales horizontally by adding more worker processes, which increases resource usage.
Real-world benchmarks show Flask handling around 2,000-3,000 requests per second on modest hardware for simple endpoints. Performance decreases as endpoint complexity increases, particularly with I/O operations.
By comparison, FastAPI's ASGI foundation allows true asynchronous request handling, which can dramatically improve performance for I/O-bound applications:
@app.get('/data')
async def get_external_data():
async with httpx.AsyncClient() as client:
# This doesn't block - other requests can be processed
response = await client.get("https://api.example.com/data")
data = response.json()
# Additional processing (ideally also async)
processed_data = await process_data_async(data)
return processed_data
FastAPI can handle multiple requests concurrently with a single worker, leading to better resource utilization.
Benchmarks consistently show FastAPI processing 15,000-20,000 requests per second on similar hardware for simple endpoints. The asynchronous design provides even more significant advantages for endpoints with multiple I/O operations.
It's important to note that FastAPI still supports synchronous handlers when needed:
@app.get('/sync-endpoint')
def sync_endpoint():
# Regular synchronous code works too
result = cpu_intensive_calculation()
return {"result": result}
This hybrid approach allows developers to use async where it provides benefits while falling back to synchronous code for CPU-bound operations.
Data validation and serialization
How each framework handles data validation reveals key philosophical differences.
Flask provides no built-in validation system, requiring you to either implement validation manually or use extensions like Flask-WTF or Marshmallow:
from flask import Flask, request, jsonify
from marshmallow import Schema, fields, validate, ValidationError
app = Flask(__name__)
class UserSchema(Schema):
name = fields.Str(required=True)
email = fields.Email(required=True)
age = fields.Integer(validate=validate.Range(min=18))
user_schema = UserSchema()
@app.route('/users', methods=['POST'])
def create_user():
try:
# Validate request data
user_data = user_schema.load(request.json)
# Process validated data
user_id = save_to_database(user_data)
return jsonify({"id": user_id, **user_data}), 201
except ValidationError as err:
return jsonify({"errors": err.messages}), 400
This approach is flexible but requires additional code and dependencies. It also separates validation from route definitions, potentially making code harder to follow.
In contrast, FastAPI integrates validation directly into route definitions using Pydantic models:
from fastapi import FastAPI, Query, Path, Body
from pydantic import BaseModel, EmailStr, Field, validator
from typing import Optional, List
app = FastAPI()
class UserCreate(BaseModel):
name: str
email: EmailStr
age: int = Field(..., ge=18)
tags: List[str] = []
@validator('name')
def name_must_not_contain_numbers(cls, v):
if any(char.isdigit() for char in v):
raise ValueError('Name cannot contain numbers')
return v
@app.post('/users')
async def create_user(
user: UserCreate,
priority: Optional[int] = Query(None, ge=1, le=5)
):
# All data is validated automatically
return {"id": 1, **user.dict(), "priority": priority}
FastAPI's validation system:
- Validates request bodies, query parameters, path parameters, and headers
- Converts data to appropriate Python types
- Provides detailed error messages automatically
- Generates OpenAPI schema documentation
- Supports complex validation with custom validators
This integration makes validation more consistent and reduces repetitive code, but it requires understanding Pydantic's modeling system.
Documentation and OpenAPI integration
Documentation is essential for API development, and the frameworks differ significantly in their approaches.
Flask has no built-in documentation generator. You typically use extensions like Flask-RESTX or Flasgger to create OpenAPI documentation:
from flask import Flask
from flasgger import Swagger, swag_from
app = Flask(__name__)
swagger = Swagger(app)
@app.route('/users/<int:user_id>')
@swag_from({
'parameters': [
{
'name': 'user_id',
'in': 'path',
'type': 'integer',
'required': True,
'description': 'ID of the user'
}
],
'responses': {
200: {
'description': 'User found',
'schema': {
'type': 'object',
'properties': {
'id': {'type': 'integer'},
'name': {'type': 'string'}
}
}
},
404: {
'description': 'User not found'
}
}
})
def get_user(user_id):
return jsonify({'id': user_id, 'name': 'Example User'})
This approach works but requires maintaining documentation separately from code, which can lead to inconsistencies as the API evolves.
As a modern alternative, FastAPI automatically generates comprehensive OpenAPI documentation from your code and type annotations:
from fastapi import FastAPI, Path, HTTPException
from pydantic import BaseModel
from typing import Optional
app = FastAPI(
title="User Management API",
description="API for managing users in the system",
version="1.0.0"
)
class User(BaseModel):
id: int
name: str
email: Optional[str] = None
is_active: bool = True
@app.get(
"/users/{user_id}",
response_model=User,
responses={
404: {"description": "User not found"},
400: {"description": "Invalid ID format"}
},
tags=["users"],
summary="Get user by ID",
description="Retrieve a user from the database by their unique identifier"
)
async def get_user(
user_id: int = Path(..., gt=0, description="The unique ID of the user")
):
if user_id == 999:
raise HTTPException(status_code=404, detail="User not found")
return {"id": user_id, "name": "Example User", "email": "user@example.com", "is_active": True}
FastAPI provides:
- Interactive documentation at
/docs
(Swagger UI) and/redoc
(ReDoc) - Detailed parameter descriptions and constraints
- Request/response schema examples
- Authentication information
- Response status codes and descriptions
The documentation automatically updates when you modify your code, ensuring it remains accurate.
Dependency injection and middleware
Both frameworks offer ways to structure code and reuse components, but with different approaches.
Flask uses a context-based system with application and request contexts. Middleware functionality is implemented through decorators like before_request
and after_request
:
@app.before_request
def authenticate():
token = request.headers.get('Authorization')
if not token:
return jsonify({"error": "Unauthorized"}), 401
# Validate token and attach user to g
g.user = validate_token(token)
@app.route('/protected')
def protected_resource():
# Access user from g
return jsonify({"message": f"Hello, {g.user['name']}"})
For reusable components, Flask developers typically use helper functions:
def get_database():
if 'db' not in g:
g.db = Database()
return g.db
@app.teardown_appcontext
def close_db(e=None):
db = g.pop('db', None)
if db is not None:
db.close()
@app.route('/items')
def get_items():
db = get_database()
items = db.query_items()
return jsonify(items)
This pattern works but relies on global state, which can complicate testing and make code harder to reason about.
Alternatively, FastAPI implements a powerful dependency injection system that makes dependencies explicit:
from fastify import FastAPI, Depends, Header, HTTPException
app = FastAPI()
async def verify_token(authorization: str = Header(...)):
if not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Invalid token format")
token = authorization.replace("Bearer ", "")
# Validate token
user = validate_token(token)
if not user:
raise HTTPException(status_code=401, detail="Invalid token")
return user
async def get_database():
db = Database()
try:
yield db
finally:
db.close()
@app.get('/protected')
async def protected_resource(user: dict = Depends(verify_token)):
return {"message": f"Hello, {user['name']}"}
@app.get('/items')
async def get_items(db: Database = Depends(get_database)):
items = await db.query_items()
return items
FastAPI’s dependency system simplifies request handling by allowing dependencies to be explicitly declared as function parameters. This approach makes code more readable and modular while enabling seamless reuse across multiple routes.
Since dependencies support async/await
, they integrate smoothly with asynchronous workflows, improving performance in high-concurrency environments. Testing becomes more straightforward by injecting mock dependencies, allowing for isolated and reliable test cases.
Additionally, dependencies can have their nested dependencies, creating a structured hierarchy that keeps complex applications organized and maintainable.
For middleware, FastAPI supports both dependency-based middleware and ASGI middleware:
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
import time
class TimingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
response.headers["X-Process-Time"] = str(process_time)
return response
app.add_middleware(TimingMiddleware)
Final thoughts
This article explored Flask and FastAPI to help you choose the right framework for your project.
FastAPI leverages async support, built-in type validation, and automatic documentation to streamline API development and improve performance. Flask, with its lightweight design and rich ecosystem of extensions, offers flexibility without enforcing a rigid structure.
If you need speed, strong typing, and automation, FastAPI is the better fit. If you prefer simplicity, a gentler learning curve, and more control over third-party integrations, Flask remains a solid choice.
Make your mark
Join the writer's program
Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.
Write for us
Build on top of Better Stack
Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github