FastAPI vs Flask - The Complete Guide
Understand why FastAPI is taking the Python community by storm
Introduction
More and more people are getting onboard the FastAPI train. Why is this? And what is it about this particular web framework that makes it worth switching away from your tried-and-tested Flask APIs?
This post compares and discusses code from an example Flask and FastAPI project. The sample project is a JSON web token (JWT) auth API. Here is the full source code.
I’m willing to concede that a better title for this post would be “why use FastAPI instead of Flask”.
Contents
1. FastAPI’s Performance
2. How FastAPI
reduces your errors with Python type declarations and Pydantic
3. FastAPI
’s elegant dependency injection
4. Automatic Documentation via Standards
5. The FastAPI
toolbox
1. FastAPI’s Performance
FastAPI’s name may lack subtlety, but it does what it says on the tin. With FastAPI, you get the sort of high-performance you would expect from traditionally faster languages like NodeJS or Go.
Naturally, benchmarks should be taken with a pinch of salt, have a look at the source of these
How is this possible in slow old Python? Under the hood, FastAPI is leveraging Python’s asyncio library, which was added in Python 3.4 and allows you to write concurrent code. Asyncio is a great fit for IO-bound network code (which is most APIs), where you have to wait for something, for example:
- Fetching data from other APIs
- Receiving data over a network (e.g. from a client browser)
- Querying a database
- Reading the contents of a file
FastAPI is built on top of Starlette, an ASGI framework created by Tom Christie (he is a Python community powerhouse who also created the Django REST Framework).
In practice, this means declaring coroutine functions with the async
keyword, and using the
await
keyword with any IO-bound parts of the code. In this regard, Flask (as of v2.x) and FastAPI are identical.
(Both frameworks use decorators to mark endpoints):
Flask:
@app.route("/get-data")
async def get_data():
data = await async_db_query(...)
return jsonify(data)
FastAPI:
@app.get('/')
async def read_results():
results = await some_library()
return results
However, Flask is fundamentally constrained in that it is a WSGI application. So whilst in newer versions of Flask (2.x) you can get a performance boost by making use of an event loop within path operations, your Flask server will still tie up a worker for each request.
FastAPI on the other hand implements the ASGI specification. ASGI is a standard interface positioned as a spiritual successor to WSGI. It enables interoperability within the whole Python async web stack: servers, applications, middleware, and individual components. See the awesome-asgi github repo for some of these resources.
With FastAPI, your application will behave in a non-blocking way throughout the stack, concurrency applies at the request/response level. This leads to significant performance improvements.
Furthermore, ASGI servers and frameworks also give you access to inherently concurrent features (WebSockets, Server-Sent Events, HTTP/2) that are impossible (or at least require workarounds) to implement using sync/WSGI. You do need to use FastAPI together with an ASGI web server - uvicorn is the recommended choice, although it’s possible to swap it out for alternatives like Hypercorn
2. How FastAPI
reduces your errors with type declarations and Pydantic schemas
In FastAPI, a combination of Python type hints and Pydantic models define your endpoint expected data schemas (both inputs and outputs).
A simple example looks like this:
@api.get('/api/add')
def calculate(x: int, y: int):
value = x + y
return {
'x': x,
'y': y,
'value': value
}
We define the endpoint parameters (x
and y
) and their types as int
and FastAPI will perform
validation on these values purely based on the Python types.
Pydantic is a library for data validation, it takes the idea of data classes a step further. FastAPI bakes Pydantic deeply into its ethos, using it for:
- Defining API config
- Defining API requests/responses (JSON schemas)
For example:
@api_router.get("/", response_model=schemas.Msg, status_code=200)
def root() -> dict:
return {"msg": "This is the Example API"}
Where the schemas.Msg
looks like this:
from pydantic import BaseModel
class Msg(BaseModel):
msg: str
Obviously this is a simple example, but now we have a very clear schema for our response format. For complex responses (or requests), we can define large Pydantic classes (and fields can be nested Pydantic models so the complexity can really ratchet up). This is great for things like machine learning APIs where you might have complex inputs to your API - here’s an example
FastAPI also uses Pydantic classes for defining app config:
class LoggingSettings(BaseSettings):
LOGGING_LEVEL: int = logging.INFO # logging levels are ints
class DBSettings(BaseSettings):
SQLALCHEMY_DATABASE_URI: str
class Settings(BaseSettings):
# 60 minutes * 24 hours * 8 days = 8 days
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
# Meta
logging: LoggingSettings = LoggingSettings()
db: SQLLiteSettings = DBSettings()
I personally like FastAPI’s opinionated config approach. Flask let’s you do pretty much anything:
- Define config in Python classes
- Parse a file, e.g. yaml, JSON, toml
Meaning you’ll often see:
class BaseConfig:
"""Base configuration."""
SECRET_KEY = os.getenv('SECRET_KEY', 'my_precious')
DEBUG = False
BCRYPT_LOG_ROUNDS = 13
SQLALCHEMY_TRACK_MODIFICATIONS = False
class DevelopmentConfig(BaseConfig):
"""Development configuration."""
DEBUG = True
BCRYPT_LOG_ROUNDS = 4
SQLALCHEMY_DATABASE_URI = postgres_local_base + database_name
In your other Python API projects you may have found yourself making use of serialization Python libraries like Marshmallow, or digging into the docs of Django REST Framework serializers.
With FastAPI, there’s no need for this. Pydantic does the job and is super intuitive (there’s no new syntax to learn). This all results in faster development speed.
3. FastAPI
’s Elegant dependency injection
What I love the most about FastAPI is its dependency injection mechanism. Dependency injection is a fancy way of saying your code has certain requirements to work. FastAPI allows you to do this at the level of path operation functions, i.e. your API routes.
This is an area where Flask is very weak. With Flask, you will often find yourself exporting globals,
or hanging values on flask.g
(which is just another global). Let’s compare the case of accessing
the database in a user auth example:
Typical Flask Approach to Dependencies
In our app __init__.py
file (visit source), we define a db
global:
import os
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app_settings = os.getenv(
'APP_SETTINGS',
'app.config.DevelopmentConfig'
)
app.config.from_object(app_settings)
db = SQLAlchemy(app)
# ...truncated
And then in our API operations (using Flask blueprints), we import that db
object:
from flask import Blueprint, request, make_response, jsonify
from flask.views import MethodView
from app import bcrypt, db, app
from app.models import User
auth_blueprint = Blueprint('auth', __name__)
class RegisterAPI(MethodView):
"""
User Registration Resource
"""
def post(self):
# get the post data
post_data = request.get_json()
# check if user already exists
user = User.query.filter_by(email=post_data.get('email')).first()
if not user:
try:
user = User(
email=post_data.get('email'),
password=post_data.get('password')
)
# insert the user
db.session.add(user)
db.session.commit()
# generate the auth token
auth_token = user.encode_auth_token(user.id)
responseObject = {
'status': 'success',
'message': 'Successfully registered.',
'auth_token': auth_token.decode()
}
return make_response(jsonify(responseObject)), 201
except Exception as e:
...
# truncated...
FastAPI Dependency Injection Example
from fastapi import Depends
@api_router.post("/signup", response_model=schemas.User, status_code=201)
def create_user_signup(
*, db: Session = Depends(deps.get_db), user_in: schemas.CreateUser,
) -> Any:
"""
Create new user without the need to be logged in.
"""
user = db.query(User).filter(User.email == user_in.email).first()
...
Notice the Depends
line, where we specify a database dependency. That dependency is a function
which looks like this:
def get_db() -> t.Generator:
db = SessionLocal() # SQLAlchemy ORM session
try:
yield db
finally:
db.close()
And that’s it. You can easily inject your database (and any other dependency you can think of) using this approach. This is a joy to test. When you set up your test app you can mock/stub dependencies with trivial adjustments:
async def override_route53_dependency() -> MagicMock:
mock = MagicMock()
return mock
@pytest.fixture()
def client() -> Generator:
with TestClient(app) as c:
app.dependency_overrides[deps.get_route53_client] = override_route53_dependency
yield c
app.dependency_overrides = {}
There are workarounds for Flask’s dependency injection shortcomings, I wrote a post about how to use the Flask-Injector library to create the same effect - but it’s so much more work and way more complexity to keep in your head than what FastAPI offers.
4. Automatic Documentation via Standards
By writing your endpoints, you are automatically writing your API documentation.
FastAPI is carefully built around the OpenAPI Specification (formerly known as swagger) standards, which means that you get great API documentation just by writing your application. No extra work. This includes:
- Path operations
- parameters
- body requests
- security
You can choose what your preferred documentation UI display as either:
Both of these options offer interactive documentation pages where you can input request data and trigger responses which is handy for bits of manual QA.
There are added benefits such as automatic client generation.
5. The FastAPI
toolbox
FastAPI is able to borrow from Starlette for more advanced functionality that you’ll often find yourself looking for in your APIs. In-process background tasks are extremely easy to implement:
@router.post("/send-password-reset", status_code=200)
def send_password_reset(
background_tasks: BackgroundTasks,
user_in: schemas.UserPasswordResetEmail,
) -> Any:
# Trigger email (asynchronous)
background_tasks.add_task(
send_password_reset_email,
user=user_in,
)
Note that the FastAPI docs make it clear these background tasks shouldn’t be used for intensive workloads, they are designed for operations that take up to a few seconds (such as sending an email).
Thanks to Starlette, you also get:
- WebSocket support
- GraphQL support
- CORS, GZip, Static Files, Streaming responses
- Session and Cookie support
Although it’s not really FastAPI’s forte, you can also use Jinja2 templates to serve dynamic HTML pages when needed. So whilst the framework is best for backend REST APIs, you have the option to serve web pages if needed.
Conclusion
If you’re looking to build APIs (especially for microservices), FastAPI is a better choice than Flask. The only reason not to use it would be if your organization already has a lot of tooling built around Flask.
If you’re building something that is a lot of server-side rendered HTML, or a CMS, then Django is probably still the way to go.
More FastAPI
- The official docs are superb
Tiangolo (Sebastián Ramírez) shoutout: If you look at some of the early reddit announcements of FastAPI in early 2019, you can see there was a lot of criticism for the project. Thankfully, Tiangolo ignored the haters and just kept building. He’s made a huge contribution to the Python ecosystem.