Jay Taylor's notes

back to listing index

Moving from Flask to FastAPI | TestDriven.io

[web search]
Original source (testdriven.io)
Tags: python Flask api web-service-development fastapi testdriven.io
Clipped on: 2023-11-08

Moving from Flask to FastAPI

Python is one of the most popular programming languages. From scripting to API development to machine learning -- Python has a footprint. Its popularity is fueled by it's focus on the developer experience and the tools it offers. Flask, a web framework, is one such tool, which is popular amongst the machine learning community. It's also widely used for API development. But there's a new framework on the rise: FastAPI. Unlike Flask, FastAPI is an ASGI (Asynchronous Server Gateway Interface) framework. On par with Go and NodeJS, FastAPI is one of the fastest Python-based web frameworks.

This article, which is aimed for those interested in moving from Flask to FastAPI, compares and contrasts common patterns in both Flask and FastAPI.

FastAPI vs Flask

FastAPI was built with these three main concerns in mind:

  1. Speed
  2. Developer experience
  3. Open standards

You can think of FastAPI as the glue that brings together Starlette, Pydantic, OpenAPI, and JSON Schema.

  • Under the hood, FastAPI uses Pydantic for data validation and Starlette for tooling, making it blazing fast compared to Flask, giving comparable performance to high-speed web APIs in Node or Go.
  • Starlette + Uvicorn offers async request capability, something that Flask lacks.
  • With Pydantic along with type hints, you get a nice editor experience with autocompletion. You also get data validation, serialization and deserialization (for building an API), and automatic documentation (via JSON Schema and OpenAPI).

That said, Flask is much more widely used, so it's battle-tested and has a much larger community supporting it. Since both frameworks are meant to be extended, Flask is the clear winner here due to it's vast plugin ecosystem.

Recommendations:

  • Use FastAPI if you resonate with the above three concerns, are tired of the plethora of choices when it comes to Flask extensions, wish to leverage async requests, or are just wanting to stand up a RESTful API.
  • Use Flask if you aren't comfortable with the maturity-level of FastAPI, need to build a full-stack app with server-side templating, or can't live without some of the community-maintained Flask extensions.

Getting Started

Installation

Like any other Python package, installation is fairly straightforward.

Flask

pip install flask

# or
poetry add flask
pipenv install flask
conda install flask

FastAPI

pip install fastapi uvicorn

# or
poetry add fastapi uvicorn
pipenv install fastapi uvicorn
conda install fastapi uvicorn -c conda-forge

Unlike Flask, FastAPI does not have a built-in development server, so an ASGI server like Uvicorn or Daphne is required.

"Hello World" App

Flask

# flask_code.py

from flask import Flask

app = Flask(__name__)

@app.route("/")
def home():
    return {"Hello": "World"}

if __name__ == "__main__":
    app.run()

FastAPI

# fastapi_code.py

import uvicorn
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def home():
    return {"Hello": "World"}

if __name__ == "__main__":
    uvicorn.run("fastapi_code:app")

Parameters like reload=True can be passed into uvicorn.run() to enable hot-reloading for development.

Alternatively, you can start the server directly form the terminal:

uvicorn run fastapi_code:app

For hot-reloading:

uvicorn run fastapi_code:app --reload

Configuration

Both Flask and FastAPI provide a number of options for dealing with different configurations for different environments. Both support the following patterns:

  1. Environment Variables
  2. Config File
  3. Instance Folder
  4. Classes and inheritance

For more, refer to their respective documentation:

Flask

import os
from flask import Flask

class Config(object):
    MESSAGE = os.environ.get("MESSAGE")

app = Flask(__name__)
app.config.from_object(Config)

@app.route("/settings")
def get_settings():
    return { "message": app.config["MESSAGE"] }

if __name__ == "__main__":
    app.run()

Now, before you run the server, set the appropriate environment variable:

export MESSAGE="hello, world"

FastAPI

import uvicorn
from fastapi import FastAPI
from pydantic import BaseSettings

class Settings(BaseSettings):
    message: str

settings = Settings()
app = FastAPI()

@app.get("/settings")
def get_settings():
    return { "message": settings.message }

if __name__ == "__main__":
    uvicorn.run("fastapi_code:app")

Again, before running the server, set the appropriate environment variable:

export MESSAGE="hello, world"

Routes, Templates, and Views

HTTP Methods

Flask

from flask import request

@app.route("/", methods=["GET", "POST"])
def home():
    # handle POST
    if request.method == "POST":
        return {"Hello": "POST"}
    # handle GET
    return {"Hello": "GET"}

FastAPI

@app.get("/")
def home():
    return {"Hello": "GET"}

@app.post("/")
def home_post():
    return {"Hello": "POST"}

FastAPI provides separate decorators for each method:

@app.get("/")
@app.post("/")
@app.delete("/")
@app.patch("/")

URL Parameters

To pass in info through the URL (like /employee/1) for managing state:

Flask

@app.route("/employee/<int:id>")
def home():
    return {"id": id}

FastAPI

@app.get("/employee/{id}")
def home(id: int):
    return {"id": id}

The URL parameter is specified similar to an f-string expression. Also, you can make use of type hints. Here, we tell Pydantic at runtime that id is of type int. In development, this can lead to better code-completion as well.

Query Parameters

Like URL parameters, query parameters (like /employee?department=sales) can also be used for managing state (usually for filtering or sorting):

Flask

from flask import request

@app.route("/employee")
def home():
    department = request.args.get("department")
    return {"department": department}

FastAPI

@app.get("/employee")
def home(department: str):
    return {"department": department}

Templates

Flask

from flask import render_template

@app.route("/")
def home():
    return render_template("index.html")

By default, Flask looks for templates in a "templates" folder.

FastAPI

You need to install Jinja:

pip install jinja2

Implementation:

from fastapi import Request
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse

app = FastAPI()

templates = Jinja2Templates(directory="templates")

@app.get("/", response_class=HTMLResponse)
def home(request: Request):
    return templates.TemplateResponse("index.html", {"request": request})

For FastAPI, you need to explicitly define the "templates" folder. Then for each response, the request context needs to be provided.

Static Files

Flask

By default, Flask serves up static files from the "static" folder.

FastAPI

In FastAPI, you need to mount a folder for static files:

from fastapi.staticfiles import StaticFiles

app = FastAPI()

app.mount("/static", StaticFiles(directory="static"), name="static")

Asynchronous Tasks

Flask

Starting with Flask 2.0, you can create asynchronous route handlers using async/await:

@app.route("/")
async def home():
    result = await some_async_task()
    return result

For more on asynchronous views in Flask, check out the Async in Flask 2.0 article.

Async in flask can also be achieved by using threads (concurrency) or multiprocessing (parallelism) or from tools like Celery or RQ:

  1. Asynchronous Tasks with Flask and Celery
  2. Asynchronous Tasks with Flask and Redis Queue

FastAPI

FastAPI greatly simplifies asynchronous tasks due to it's native support for asyncio. To use, simply add the async keyword to the view function:

@app.get("/")
async def home():
    result = await some_async_task()
    return result

FastAPI also has a background tasks feature, which you can use to define background tasks to be run after returning a response. This is useful for operations that don't need to complete before the response is sent back.

from fastapi import BackgroundTasks

def process_file(filename: str):
    # process file :: takes minimum 3 secs (just an example)
    pass

@app.post("/upload/{filename}")
async def upload_and_process(filename: str, background_tasks: BackgroundTasks):
    background_tasks.add_task(process_file, filename)
    return {"message": "processing file"}

Here, the response will be sent instantly without making the user wait for the file processing to complete.

You may want to use Celery instead of BackgroundTasks when you need to perform heavy background computations or if you require a task queue to manage the tasks and workers. For more, refer to Asynchronous Tasks with FastAPI and Celery.

Dependency Injection

Flask

Although you can implement your own Dependency Injection solution, Flask does not have true first-class support for it by default. Instead, you'll want to use an external package like flask-injector.

FastAPI

FastAPI, on the other hand, has a powerful solution for handling Dependency Injection.

For example:

from databases import Database
from fastapi import Depends
from starlette.requests import Request

from db_helpers import get_all_data
def get_db(request: Request):
    return request.app.state._db

@app.get("/data")
def get_data(db: Database = Depends(get_db)):
    return get_all_data(db)

So, get_db will grab a reference to the database connection create in the app's startup event handler. Depends is then used to indicate to FastAPI that the route "depends" on get_db. So, it should be executed before the code in the route handler and the result should be "injected" into the route itself.

Data Validation

Flask

Flask does not have any inhouse data validation support. You can use the powerful Pydantic package for data validation through Flask-Pydantic.

FastAPI

One of the things that makes FastAPI so powerful is it's support for Pydantic.

from pydantic import BaseModel

app = FastAPI()

class Request(BaseModel):
    username: str
    password: str

@app.post("/login")
async def login(req: Request):
    if req.username == "testdriven.io" and req.password == "testdriven.io":
        return {"message": "success"}
    return {"message": "Authentication Failed"}

Here, we take in an input of model Request. The payload must contain a username and password.

# correct payload format
✗ curl -X POST 'localhost:8000/login' 
    --header 'Content-Type: application/json' 
    --data-raw '{"username": "testdriven.io","password":"testdriven.io"}'

{"message":"success"}

# incorrect payload format
✗ curl -X POST 'localhost:8000/login' 
    --header 'Content-Type: application/json' 
    --data-raw '{"username": "testdriven.io","passwords":"testdriven.io"}'

{"detail":[{"loc":["body","password"],"msg":"field required","type":"value_error.missing"}]}

Take note of the request. We passed in passwords as a key instead of password. The Pydantic model automatically tells the user that the password field is missing.

Serialization and Deserialization

Flask

The easiest way to serialize is to use jsonify:

from flask import jsonify
from data import get_data_as_dict

@app.route("/")
def send_data():
    return jsonify(get_data_as_dict)

For complex objects, Flask developers often use Flask-Marshmallow.

FastAPI

FastAPI automatically serializes any returned dict. For more complex and structured data, Pydantic is used:

from pydantic import BaseModel

app = FastAPI()

class Request(BaseModel):
    username: str
    email: str
    password: str

class Response(BaseModel):
    username: str
    email: str

@app.post("/login", response_model=Response)
async def login(req: Request):
    if req.username == "testdriven.io" and req.password == "testdriven.io":
        return req
    return {"message": "Authentication Failed"}

Here, we added a Request model with three inputs: username, email, and password. We also defined a Response model with just the username and email. The input Request model handles deserialization while the output Response model handles object serialization. The response model is then passed in to the decorator via the response_model parameter.

Now if we return the request itself as the response, Pydantic will omit the password, because our defined response model does not contain a password field.

Example:

# output
✗ curl -X POST 'localhost:8000/login' 
    --header 'Content-Type: application/json' 
    --data-raw '{"username":"testdriven.io","email":"[email protected]","password":"testdriven.io"}'

{"username":"testdriven.io","email":"[email protected]"}

Middleware

Middleware is used to apply logic with every request before it's processed by the view function.

Flask

class middleware:
    def __init__(self, app) -> None:
        self.app = app

    def __call__(self, environ, start_response):
        start = time.time()
        response = self.app(environ, start_response)
        end = time.time() - start
        print(f"request processed in {end} s")
        return response

app = Flask(__name__)
app.wsgi_app = middleware(app.wsgi_app)

FastAPI

from fastapi import Request

@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    start_time = time.time()
    response = await call_next(request)
    process_time = time.time() - start_time
    print(f"request processed in {process_time} s")
    return response

The @app.middleware("http") decorator is a must for creating middleware in FastAPI. The above middleware calculates the time taken for processing a request. After the request is processed by the view function, the total processing time is calculated and sent back as a response header.

# flask output(logs)
request processed in 0.0010077953338623047 s
127.0.0.1 - - [22/Sep/2020 18:56:21] "GET / HTTP/1.1" 200 -

# fastapi output(logs)
request processed in 0.0009925365447998047 s
INFO:     127.0.0.1:51123 - "GET / HTTP/1.1" 200 OK

Modularity

As an app grows, at some point you'll want to group similar views, templates, static files, and models together to help decompose the app into smaller components.

Flask

In Flask, Blueprints are used for modularity:

# blueprints/product/views.py
from flask import Blueprint

product = Blueprint("product", __name__)

@product.route("/product1")
    ...
# main.py

from blueprints.product.views import product

app.register_blueprint(product)

FastAPI

With FastAPI, meanwhile, modularity is achieved via an APIRouter:

# routers/product/views.py
from fastapi import APIRouter

product = APIRouter()

@product.get("/product1")
    ...
# main.py

from routers.product.views import product

app.include_router(product)

Additional Features

Automatic Documentation

Flask

Flask does not automatically create API documentation out-of-the-box. However, there are several extensions that handle this like flask-swagger and Flask RESTX but they require additional setup.

FastAPI

FastAPI supports OpenAPI along with Swagger UI and ReDoc by default. This means that every endpoint is automatically documented from the metadata associated with the endpoint.

All the registered endpoints are listed here

Alternative documentation

Admin app

Flask

Flask has a widely used third party admin package called Flask-Admin, which is used to quickly perform CRUD operations against your models.

FastAPI

As of writing, there are two popular FastAPI extensions for this:

  1. FastAPI Admin - Functional admin panel that provides a user interface for performing CRUD operations on your data.
  2. SQLAlchemy Admin - Admin Panel for FastAPI/Starlette that works with SQLAlchemy models.

Authentication

Flask

While Flask doesn't have a native solution, several third-party extensions are available.

FastAPI

FastAPI natively supports a number of security and authentication tools via the fastapi.security package. With a few lines of code, you can add basic HTTP authentication to your application:

import secrets

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()

security = HTTPBasic()


def get_current_username(credentials: HTTPBasicCredentials = Depends(security)):
    correct_username = secrets.compare_digest(credentials.username, "stanleyjobson")
    correct_password = secrets.compare_digest(credentials.password, "swordfish")
    if not (correct_username and correct_password):
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
    return credentials.username


@app.get("/whoami")
def who_ami_i(username: str = Depends(get_current_username)):
    return {"username": username}

FastAPI implements OAuth2 and OpenID Connect via the OpenAPI standards.

Review the following resources from the official documentation for more:

  1. Security Intro
  2. Advanced Security

Additional Resources

  1. Web Authentication Methods Compared
  2. Adding Social Authentication to Flask
  3. Session-based Auth with Flask for Single Page Apps
  4. Securing FastAPI with JWT Token-based Authentication

CORS

CORS (Cross-Origin Resource Sharing) middleware checks whether or not that requests are coming from allowed origins. If yes, the request is passed along to the next middleware or to the view function. If not, it rejects the request, which sends an error response back to the caller.

Flask

Flask requires an external package called Flask-CORS for CORS support:

pip install flask-cors

Basic implementation:

from flask_cors import CORS

app = Flask(__name__)

CORS(app)

FastAPI

FastAPI natively supports CORS:

from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

origins = ["*"]

app.add_middleware(CORSMiddleware, allow_origins=origins)

Testing

Flask

import pytest
from flask import Flask

app = Flask(__name__)

@app.route("/")
def home():
    return {"message": "OK"}

def test_hello():
    res = app.test_client().get("/")

    assert res.status_code == 200
    assert res.data == b'{"message":"OK"}n'

FastAPI

from fastapi import FastAPI
from fastapi.testclient import TestClient

app = FastAPI()

@app.get("/")
async def home():
    return {"message": "OK"}

client = TestClient(app)

def test_home():
    res = client.get("/")

    assert res.status_code == 200
    assert res.json() == {"message": "OK"}

FastAPI provides a TestClient. With it, you can run pytest directly with FastAPI. For more information, review the Testing guide from the official documentation.

Deployment

Production Server

Flask

Flask by default runs a development WSGI (Web Server Gateway Interface) application server. For production, you'll need to use a production-grade WSGI app server like Gunicorn, uWSGI, or mod_wsgi

Install Gunicorn:

pip install gunicorn

Start server:

# main.py
# app = Flask(__name__)

gunicorn main:app

FastAPI

Since FastAPI doesn't have a development server, you'll use Uvicorn (or Daphne) for both development and production.

Install Uvicorn:

pip install uvicorn

Start server:

# main.py
# app = FastAPI()

uvicorn main:app

You may want to use Gunicorn to manage Uvicorn in order to take advantage of both concurrency (via Uvicorn) and parallelism (via Gunicorn workers):

# main.py
# app = FastAPI()

gunicorn -w 3 -k uvicorn.workers.UvicornWorker main:app

Docker

Flask

FROMpython3.10-slim

WORKDIR/app

COPYrequirements.txt .

RUNpip install -r requirements.txt

COPY. .

EXPOSE5000

CMD["gunicorn","main:app"]

This is one of the simplest Dockerfiles for Flask. To see how to fully configure it for production, review the Dockerizing Flask with Postgres, Gunicorn, and Nginx tutorial.

FastAPI

FROMpython3.10-slim

WORKDIR/app

COPYrequirements.txt .

RUNpip install -r requirements.txt

COPY. .

EXPOSE8000

CMD["uvicorn","main:app"]

Again, this is a very simple configuration. The FastAPI author has provided several production-ready Dockerfiles. For more, review the official FastAPI documentation as well as the Dockerizing FastAPI with Postgres, Uvicorn, and Traefik tutorial.

Conclusion

Taking a step back, Django and Flask are the two most popular Python-based web frameworks (FastAPI is the third most popular). They (Django and Flask) have very different philosophies, though. The advantage of Flask over Django is that Flask is a micro-framework. The program structure is left to the programmers' discretion and not enforced. Developers can add third-party extensions to improve their code as they see fit. That said, typically, as the code base grows, there's a need for a number of common features that almost all web apps need. Tight integration of these features with the framework results in much less code that end developers need to create and maintain on their own.

The code examples throughout this article convey the same thing. In other words, FastAPI includes many of the required features. It also follows strict standards, making your code production-ready and easier to maintain. FastAPI is also very well-documented.

While FastAPI may not be as battle-tested as Flask, more and more developers are turning to it for serving up a machine learning model or developing a RESTful API. Switching to FastAPI is a solid choice.

Official Documentation

Additional Resources