Intermediate
You need to build a Python API. You Google “Python web framework” and land on two names repeatedly: Flask and FastAPI. Flask has been the go-to choice for years and has thousands of tutorials. FastAPI is newer and claims to be one of the fastest Python frameworks available. Both will let you build an API, but they make different trade-offs, and choosing the wrong one means fighting the framework for the entire life of your project.
Both frameworks are pip-installable and require no special setup. Flask needs pip install flask. FastAPI needs pip install fastapi uvicorn — uvicorn is the ASGI server that runs FastAPI applications. Neither one requires a database or ORM to get started, and both work well with SQLAlchemy, Redis, or any other Python data layer you prefer.
This article covers what each framework actually does differently, shows the same API endpoint built in both, compares performance for I/O-bound and CPU-bound workloads, explains the automatic documentation feature that FastAPI ships by default, and ends with a concrete decision guide. By the end you will have a clear answer for your specific situation rather than another list of bullet points that could go either way.
Side-by-Side Quick Example
The fastest way to understand the difference is to see the same endpoint in both frameworks. Here is a user lookup endpoint that validates the request, queries a data source, and returns a typed response:
Flask version:
# flask_app.py
from flask import Flask, jsonify, abort
app = Flask(__name__)
USERS = {
1: {"id": 1, "name": "Alice", "email": "alice@example.com"},
2: {"id": 2, "name": "Bob", "email": "bob@example.com"},
}
@app.route("/users/<int:user_id>")
def get_user(user_id):
user = USERS.get(user_id)
if not user:
abort(404, description=f"User {user_id} not found")
return jsonify(user)
if __name__ == "__main__":
app.run(debug=True)
FastAPI version:
# fastapi_app.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class User(BaseModel):
id: int
name: str
email: str
USERS = {
1: User(id=1, name="Alice", email="alice@example.com"),
2: User(id=2, name="Bob", email="bob@example.com"),
}
@app.get("/users/{user_id}", response_model=User)
async def get_user(user_id: int):
user = USERS.get(user_id)
if not user:
raise HTTPException(status_code=404, detail=f"User {user_id} not found")
return user
Both do the same job: look up a user by ID and return 404 if not found. The FastAPI version has more code, but it gains three things the Flask version does not have: automatic input validation (user_id is guaranteed to be an integer), a defined response schema (the User model), and a free interactive API documentation page at /docs. Whether those extras are worth the additional setup is the core of the comparison.
What Flask and FastAPI Actually Are
Flask was released in 2010 as a micro-framework — it provides routing, a development server, and a request/response abstraction, and then gets out of your way. Everything else (authentication, validation, serialization, database access) is your problem. This minimalism is a deliberate design choice, not a limitation. It means you can wire together exactly the libraries you want without the framework imposing opinions.
FastAPI was released in 2018 and is built on top of Starlette (an ASGI framework) and Pydantic (a data validation library). It is not a micro-framework — it has built-in opinions about request validation, response serialization, and API documentation. It is designed specifically for building APIs, not general web applications, and it shows in every design decision the framework makes.
| Feature | Flask | FastAPI |
|---|---|---|
| First released | 2010 | 2018 |
| Server type | WSGI (sync) | ASGI (async) |
| Request validation | Manual | Automatic (Pydantic) |
| Response serialization | Manual (jsonify) | Automatic (Pydantic) |
| API documentation | Not included | Built-in (Swagger + ReDoc) |
| Type hints required | No | Yes (for full benefit) |
| Async support | Limited (Flask 2.0+) | Native |
| Learning curve | Low | Medium |
| GitHub stars (2024) | 66k+ | 73k+ |
The server type difference is the biggest technical distinction. WSGI (Web Server Gateway Interface) is synchronous — one request is handled, then the next. ASGI (Asynchronous Server Gateway Interface) allows a single process to handle thousands of requests concurrently using async/await. For I/O-bound APIs (database queries, HTTP calls to other services), this difference is significant. For purely CPU-bound work, it matters less.
Performance: Where the Gap Actually Appears
Performance benchmarks between FastAPI and Flask require context to be meaningful. The gap is real but only shows up in specific scenarios. Here is what the difference looks like in practice, using a simulated I/O-bound endpoint:
# bench_flask.py (run with: flask run --port 5000)
import time
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/slow")
def slow_endpoint():
time.sleep(0.05) # simulate 50ms DB query
return jsonify({"result": "done"})
# bench_fastapi.py (run with: uvicorn bench_fastapi:app --port 8000)
import asyncio
from fastapi import FastAPI
app = FastAPI()
@app.get("/slow")
async def slow_endpoint():
await asyncio.sleep(0.05) # non-blocking 50ms wait
return {"result": "done"}
Running both with a tool like httpbin.org‘s wrk or Python’s httpx at 100 concurrent requests reveals the fundamental difference. The Flask endpoint with time.sleep() blocks the entire process for 50ms per request — concurrent requests queue up. The FastAPI endpoint with asyncio.sleep() releases the event loop during the wait, allowing other requests to be handled simultaneously.
# load_test_compare.py
# Run BOTH servers first, then run this comparison
import asyncio
import httpx
import time
async def measure_throughput(url, concurrent=50, total=200):
"""Send `total` requests with `concurrent` in-flight at once."""
semaphore = asyncio.Semaphore(concurrent)
results = []
async def fetch(client):
async with semaphore:
start = time.perf_counter()
r = await client.get(url)
elapsed = time.perf_counter() - start
results.append(elapsed)
async with httpx.AsyncClient(timeout=30.0) as client:
start_all = time.perf_counter()
await asyncio.gather(*[fetch(client) for _ in range(total)])
total_time = time.perf_counter() - start_all
avg = sum(results) / len(results)
rps = total / total_time
print(f"{url}")
print(f" Total time: {total_time:.2f}s for {total} requests")
print(f" Req/sec: {rps:.0f}")
print(f" Avg latency: {avg*1000:.0f}ms")
print()
async def main():
await measure_throughput("http://localhost:5000/slow")
await measure_throughput("http://localhost:8000/slow")
asyncio.run(main())
Typical output (50 concurrent, 200 total requests):
http://localhost:5000/slow
Total time: 10.42s for 200 requests
Req/sec: 19
Avg latency: 2489ms
http://localhost:8000/slow
Total time: 0.63s for 200 requests
Req/sec: 317
Avg latency: 156ms
That is a 16x throughput difference for this specific workload. The gap is large because the I/O is simulated as blocking in Flask and non-blocking in FastAPI. In production, the actual difference depends on what fraction of your endpoint time is spent waiting on I/O (database, cache, external APIs) vs. running CPU-bound Python code. If your endpoints are mostly CPU-bound, the gap narrows significantly because async does not help with Python compute — the GIL still prevents true parallel execution.
Automatic Validation and Documentation
FastAPI’s most practical advantage over Flask is not performance — it is that you get automatic request validation and interactive documentation for free. Here is what that looks like with a more realistic endpoint that accepts a POST body:
# fastapi_with_validation.py
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr, field_validator
from typing import Optional
app = FastAPI(title="User API", version="1.0.0")
class CreateUserRequest(BaseModel):
name: str
email: str
age: int
role: Optional[str] = "user"
@field_validator("age")
@classmethod
def age_must_be_positive(cls, v):
if v < 0 or v > 150:
raise ValueError("age must be between 0 and 150")
return v
@field_validator("role")
@classmethod
def role_must_be_valid(cls, v):
allowed = {"user", "admin", "moderator"}
if v not in allowed:
raise ValueError(f"role must be one of {allowed}")
return v
class UserResponse(BaseModel):
id: int
name: str
email: str
role: str
@app.post("/users", response_model=UserResponse, status_code=201)
async def create_user(body: CreateUserRequest):
# FastAPI has already validated body -- you get clean data here
new_id = 42 # in production, this comes from your DB
return UserResponse(id=new_id, name=body.name, email=body.email, role=body.role)
Test it with a bad request to see validation in action:
# test_validation.py
import httpx
base = "http://localhost:8000"
# Bad request: age is negative, role is invalid
bad = {"name": "Charlie", "email": "charlie@example.com", "age": -5, "role": "superuser"}
r = httpx.post(f"{base}/users", json=bad)
print(r.status_code)
print(r.json())
Output:
422
{'detail': [{'type': 'value_error', 'loc': ['body', 'age'], 'msg': 'Value error, age must be between 0 and 150', 'input': -5, 'url': 'https://errors.pydantic.dev/2.6/v/value_error'}, {'type': 'value_error', 'loc': ['body', 'role'], 'msg': "Value error, role must be one of {'user', 'admin', 'moderator'}", 'input': 'superuser', 'url': 'https://errors.pydantic.dev/2.6/v/value_error'}]}
FastAPI automatically returns HTTP 422 with a detailed error breakdown when validation fails — you did not write any error handling code for this. In Flask, you would need to validate request.json manually, write the error response yourself, and make sure every endpoint consistently returns the same error format. Over dozens of endpoints, that inconsistency compounds.
The automatic documentation is equally valuable. When you run the FastAPI app above and visit http://localhost:8000/docs, you see an interactive Swagger UI showing every endpoint, its expected request body schema (with your field validators documented), and a form to test requests directly in the browser. http://localhost:8000/redoc shows the same information in ReDoc format, which is better for sharing with API consumers who are not developers.
Where Flask Is Still the Right Choice
FastAPI’s advantages are real, but Flask is not obsolete. There are scenarios where Flask is still the better choice:
Simple internal tooling: If you are building a webhook receiver, an internal dashboard backend, or a small utility API that only you and one other service will call, Flask’s simplicity gets you there faster. You do not need Pydantic models, you do not need async, and you do not need generated documentation. A Flask app can be a single file with five routes that does exactly what it needs to do and nothing more.
Teams unfamiliar with type hints: FastAPI’s full benefit requires that your team write Python with type annotations. If your team is not there yet, FastAPI becomes a source of friction rather than productivity. You can use FastAPI without type hints, but then you are paying the framework’s learning curve without getting its main advantages. Flask has no such prerequisite.
Existing Flask ecosystem requirements: Flask has a mature extension ecosystem: Flask-Login, Flask-SQLAlchemy, Flask-Admin, Flask-Mail, and dozens of others that have been production-tested for years. If your project needs functionality that has a high-quality Flask extension with no FastAPI equivalent, the extension ecosystem is a legitimate reason to stay with Flask.
HTML-rendering web applications: FastAPI is designed for APIs. If your application renders HTML server-side with templates (Jinja2), Flask’s integration with its templating system is more natural. FastAPI supports Jinja2 templates, but the ergonomics are better in Flask for traditional web applications that mix HTML pages with API endpoints.
Where FastAPI Is the Right Choice
FastAPI earns its keep when any of these apply to your project:
High-concurrency APIs: Any API that spends time waiting on external services — databases with connection pools, third-party APIs, Redis — benefits from FastAPI’s async model. A single FastAPI process with asyncio can handle hundreds of concurrent I/O-bound requests that would queue up in Flask.
APIs with complex schemas: When your request and response bodies have more than a few fields, Pydantic models pay for themselves quickly. Nested objects, optional fields, validators, and discriminated unions are all expressed as Python classes, and the API documentation is generated from them automatically. In Flask, maintaining the code and keeping the documentation accurate are separate jobs.
APIs consumed by other developers: The auto-generated OpenAPI schema that FastAPI produces at /openapi.json can be imported into Postman, used to generate client SDKs in any language, or shared with frontend developers who want to know what the API expects. This saves significant back-and-forth in teams where the API developer and the API consumer are different people.
Machine learning model serving: FastAPI has become the standard framework for serving ML models in Python. The pattern is straightforward: load the model at startup in a lifespan event handler, define a Pydantic model for the input features, and return predictions in a typed response. FastAPI’s async model handles concurrent inference requests efficiently, and the documentation makes the input schema self-explanatory to other team members.
Real-Life Example: Task Queue API in Both Frameworks
Here is the same task submission API built in both Flask and FastAPI. This demonstrates the practical code difference for a realistic endpoint that accepts work, validates it, stores it, and returns a task ID:
# tasks_flask.py -- flask run --port 5000
from flask import Flask, request, jsonify, abort
import uuid, time
app = Flask(__name__)
TASKS = {}
@app.route("/tasks", methods=["POST"])
def create_task():
data = request.get_json(silent=True)
if not data:
abort(400, "Request body must be JSON")
if "command" not in data:
abort(400, "Missing required field: command")
if not isinstance(data["command"], str) or not data["command"].strip():
abort(400, "command must be a non-empty string")
priority = data.get("priority", "normal")
if priority not in ("low", "normal", "high"):
abort(400, "priority must be low, normal, or high")
task_id = str(uuid.uuid4())
TASKS[task_id] = {
"id": task_id,
"command": data["command"].strip(),
"priority": priority,
"status": "queued",
"created_at": time.time(),
}
return jsonify(TASKS[task_id]), 201
@app.route("/tasks/<task_id>")
def get_task(task_id):
task = TASKS.get(task_id)
if not task:
abort(404, f"Task {task_id} not found")
return jsonify(task)
# tasks_fastapi.py -- uvicorn tasks_fastapi:app --port 8000
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, field_validator
from typing import Literal
from datetime import datetime
import uuid
app = FastAPI(title="Task Queue API")
TASKS: dict = {}
class CreateTaskRequest(BaseModel):
command: str
priority: Literal["low", "normal", "high"] = "normal"
@field_validator("command")
@classmethod
def command_must_not_be_empty(cls, v):
if not v.strip():
raise ValueError("command must not be empty")
return v.strip()
class TaskResponse(BaseModel):
id: str
command: str
priority: str
status: str
created_at: datetime
@app.post("/tasks", response_model=TaskResponse, status_code=201)
async def create_task(body: CreateTaskRequest):
task_id = str(uuid.uuid4())
task = TaskResponse(
id=task_id, command=body.command, priority=body.priority,
status="queued", created_at=datetime.now()
)
TASKS[task_id] = task
return task
@app.get("/tasks/{task_id}", response_model=TaskResponse)
async def get_task(task_id: str):
task = TASKS.get(task_id)
if not task:
raise HTTPException(404, detail=f"Task {task_id} not found")
return task
The Flask version has 36 lines and does manual validation on every field. The FastAPI version has 42 lines but the validation logic is reusable and the Literal["low", "normal", "high"] type annotation eliminates an entire if-statement. As the number of endpoints grows, the FastAPI version scales better — adding a new endpoint means defining a Pydantic model and a function, not writing another block of request.get_json() validation code.
Frequently Asked Questions
Can I migrate an existing Flask app to FastAPI?
Yes, gradually. FastAPI and Flask are both WSGI/ASGI servers that can run alongside each other, and a FastAPI app can mount a Flask app as a sub-application using the WSGIMiddleware from starlette.middleware.wsgi. The practical approach is to route new endpoints through FastAPI while keeping existing Flask routes intact, then migrate old routes one at a time. A full migration requires adding Pydantic models for all your request/response schemas, which is the most time-consuming part.
Flask 2.0 added async views — is the performance the same as FastAPI now?
No. Flask’s async support uses a thread pool under the hood rather than a native event loop. Each async Flask view runs in a separate thread, which adds overhead and does not scale as well as FastAPI’s native ASGI event loop. It is better than blocking synchronous Flask, but the concurrency model is fundamentally different. Flask with async views is suitable for reducing blocking in specific endpoints; FastAPI is the correct choice when high concurrent throughput is the actual goal.
Which framework should beginners learn first?
Flask, because it has less initial surface area. You can build a working API with just @app.route, request.json, and jsonify — three concepts. FastAPI requires understanding type annotations, Pydantic models, async/await, and dependency injection before you see the full benefit. Once you understand how web frameworks work from Flask, picking up FastAPI takes a day or two. Going straight to FastAPI without web framework foundations is harder than necessary.
Is testing different between the two frameworks?
Similar in structure, different in tools. Flask uses its built-in test client (app.test_client()) or the requests library with a live server. FastAPI provides a TestClient built on httpx (from Starlette). Both support writing endpoint tests as simple function calls without needing a running server. FastAPI’s dependency injection system makes it easier to swap out dependencies (like database sessions) during testing — you override a dependency with a mock version at the test level rather than monkey-patching.
Do they deploy differently?
Flask deploys with any WSGI server: Gunicorn, uWSGI, or mod_wsgi. The command is typically gunicorn app:app -w 4. FastAPI deploys with any ASGI server: Uvicorn, Hypercorn, or Daphne. The command is uvicorn app:app --workers 4. Both integrate identically with Docker, systemd, Heroku, and cloud platforms like AWS, GCP, and Azure. The container image size and configuration are nearly identical. If you have Flask deployment experience, FastAPI deployment will feel familiar.
Which framework works better with SQLAlchemy?
Both work well. SQLAlchemy 2.0 added native async support (AsyncSession, create_async_engine), which pairs naturally with FastAPI. Flask-SQLAlchemy is a mature extension that handles session management automatically within request context. For new projects using async database access, FastAPI + SQLAlchemy 2.0 async is the modern stack. For projects with existing synchronous SQLAlchemy code, Flask is easier to integrate without rewriting the data layer.
Conclusion
Both Flask and FastAPI are production-ready frameworks used at scale in real applications. The choice is not about which one is better in the abstract — it is about which one fits your team’s current skill level, your API’s concurrency requirements, and how much you need the automatic documentation.
Choose Flask when you want to move fast with minimal prerequisites, when the project is small or internal, or when your team is not yet writing type-annotated Python. Choose FastAPI when you are building an API that will handle significant concurrent load, when the API will be consumed by other developers who need documentation, or when you want automatic input validation without writing it yourself. Either framework will scale to production — the difference is in the day-to-day developer experience and the specific technical constraints of your use case.
The official documentation for each framework is the best next step: Flask documentation covers its extension ecosystem in depth, and the FastAPI documentation has excellent tutorials for dependency injection, background tasks, and security patterns that this article did not cover.