The online course, "Testing and Monitoring ML Model Deployments" is now live.

FastAPI vs Flask - The Complete Guide

Understand why FastAPI is taking the Python community by storm
Created: 16 June 2021
Last updated: 30 June 2021

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.0

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

FastAPI Github stars


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.

FastAPI benchmarks 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

uvicorn logo

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

DI meme

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:

Swagger UI documentation

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.

Category