Skip to content

Migrating from Flask to FastAPI, Part 2

Here's the second part of the blog series about the migration from Flask to FastAPI.

If you didn't read the first part, it's here: Migrating from Flask to FastAPI, Part 1.

In this one we'll see all the main code changes and refactors you would need to do.

Simple Flask to FastAPI

For an overview of most of the simple changes in how you would build a web API in Flask and FastAPI, you can see this article: Moving from Flask to FastAPI on TestDriven.io.

Here you will see a short overview of the main differences, but most importantly, you'll see how to migrate an existing code base that depends on deep integrations with Flask and how to move to FastAPI.

Install Flask

You will probably install Flask with something like:

$ pip install flask

---> 100%

And you will probably also install a server application (a WSGI server) like Gunicorn or uWSGI:

$ pip install gunicorn

---> 100%

Later you would run your Flask application in some way with Gunicorn (or a similar server application), probably something like:

$ gunicorn main:app

Starting gunicorn 20.1.0
Listening at: http://127.0.0.1:8000 (31184)
Using worker: sync
Booting worker with pid: 31217

Install FastAPI

In the case of FastAPI, you would install it and also Uvicorn or a similar server (an ASGI server, instead of a WSGI server).

If you install fastapi[all] that will include Uvicorn:

$ pip install "fastapi[all]"

---> 100%

And then, similarly, you would run your application with Uvicorn:

$ uvicorn main:app

Started server process [31799]
Waiting for application startup.
Application startup complete.
Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

Basic Flask app

In your Flask application you might have some code like this:

from flask import Flask, jsonify

app = Flask(__name__)


@app.route("/", methods=["GET"])
def root():
    return jsonify({"message": "Hello World"})

Basic FastAPI app

FastAPI takes a lot of inspiration from Flask, so it would look quite similar:

from fastapi import FastAPI

app = FastAPI()


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

Flask Path Parameters

To declare path parameters in Flask, you use a special syntax to define the parameter and the type.

And then you get the data in a parameter in the function.

from flask import Flask, jsonify

app = Flask(__name__)


@app.route("/items/<int:item_id>", methods=["GET"])
def read_item(item_id):
    return jsonify({"item_id": item_id})

FastAPI Path Parameters

In FastAPI, the syntax for path parameters is the same one you would use for f-strings, and the type is defined in the standard type annotations in the function parameters:

from fastapi import FastAPI

app = FastAPI()


@app.get("/items/{item_id}")
def read_item(item_id: int):
    return {"item_id": item_id}

Flask Query Parameters

For query parameters things start to be a little different.

Flask has a request object, it is a pseudo-global variable, and it has different data for each actual request.

You import it and use it directly in your code, and Flask does some black magic underneath to put the data in that special object.

Then, in this request object, you access the request.args dictionary, it contains any query parameters passed in the URL:

from flask import Flask, jsonify, request

app = Flask(__name__)


@app.route("/items/<int:item_id>", methods=["GET"])
def read_item(item_id):
    pretty = request.args["pretty"]
    return jsonify({"item_id": item_id, "pretty": pretty})

This would expect that the URL is called with a query parameter pretty, like:

https://example.com/items/5?pretty=true

Info

If the query parameter is not sent, that would create a special KeyError, if you don't handle it, that will result in an automatic response of 400 Bad Request.

FastAPI Query Parameters

In FastAPI, you define the query parameters and their types in the same function parameters:

from fastapi import FastAPI

app = FastAPI()


@app.get("/items/{item_id}")
def read_item(item_id: int, pretty: bool):
    return {"item_id": item_id, "pretty": pretty}

Info

In this case, pretty doesn't have a default value, so it's required. If the client doesn't send it, they will receive an error explaining that pretty is a required query parameter value of type boolean.

Flask Query Parameter Types and Defaults

When you are building a web application, and especially a web API, you probably need to do some data validation and conversion.

If pretty was a boolean query parameter that should not be required, and by default would be just False, you could write that in Flask like this:

from flask import Flask, jsonify, request

app = Flask(__name__)


@app.route("/items/<int:item_id>", methods=["GET"])
def read_item(item_id):
    pretty_str = request.args.get("pretty", "")
    if pretty_str.lower() in {"", "false", "0"}:
        pretty = False
    else:
        pretty = True
    return jsonify({"item_id": item_id, "pretty": pretty})

This is a simple example, in this case, pretty could be an empty string, or the string "false", or a "0", and it would mean it's False, otherwise, any other value would be True.

And by default, pretty would be considered to be False.

You can probably think of other edge cases and more validation code to write, but this would be enough for this example.

FastAPI Query Parameter Types and Defaults

This is one of the use cases where FastAPI's default data validation, conversion and documentation could be beneficial.

You define the query parameter in the function, by defining the type, FastAPI will do all the data validation and conversion necessary.

And if you define a default value in the function parameters, that will be the default value. And by having a default value it also means that it will not be required.

from fastapi import FastAPI

app = FastAPI()


@app.get("/items/{item_id}")
def read_item(item_id: int, pretty: bool = False):
    return {"item_id": item_id, "pretty": pretty}

This is probably one of the situations where if you had two options:

  • improve the Flask API code to add more validation and testing before migrating
  • or just migrate directly to FastAPI

...in some cases it would probably make sense to just migrate to FastAPI, as there's less work to do and you already have certain guarantees about data validation and types.

Tip

FastAPI uses Pydantic to do all the data validation, serialization, and documentation.

Flask POST

In Flask, to receive an HTTP POST request you would add that to the list of methods. And Flask would allow you to use the same function for different HTTP methods.

from flask import Flask, jsonify

app = Flask(__name__)


@app.route("/items/", methods=["POST"])
def create_item():
    return jsonify({"message": "Hello World"})

FastAPI POST

In FastAPI, you define the HTTP method or operation (two terms for the same thing) in the same decorator. FastAPI discourages you from sharing the same code for different operations.

So, you would normally have one function for each method/operation instead of having code that could handle both GET and POST.

from fastapi import FastAPI

app = FastAPI()


@app.post("/items/")
def create_item():
    return {"message": "Hello World"}

Because each of these individual functions handles a specific path with a specific HTTP operation in FastAPI it's called a path operation.

If you have code that handles multiple operations, like GET and POST in the same function, then this is a good moment to split that, FastAPI will enforce you to do it. You will probably end up with cleaner code that doesn't have to mix too much independent logic and that will probably reduce unnecessary complexity in your code.

Flask Request Body

In Flask, to get JSON data sent in a request, you would obtain it from the request object, and then you would check the data to see if it contains what you expect.

You would probably have some validation logic in the code to make sure that you received the data you expected, or to convert the data to the correct values if it's not already correct.

For example, you could write something like this:

from flask import Flask, jsonify, request, abort

app = Flask(__name__)


@app.route("/items/", methods=["POST"])
def create_item():
    data = request.get_json()
    if "name" not in data:
        abort(400)
    item_name = data["name"]
    try:
        item_size = int(data.get("size", 0))
    except ValueError:
        abort(400)
    tags = set(data.get("tags", []))
    return jsonify({"name": item_name, "size": item_size, "tags": tags})

This is some simple validation, it doesn't cover other cases, for example:

  • if the request body is a JSON array instead of a JSON object
  • if the name field is a JSON object instead of a string
  • if the tags field is a string instead of a JSON array of strings

Those would be additional corner cases that could cause problems in your code and that would probably need some additional validations.

FastAPI Request Body

In FastAPI, you define the data shape and type you expect using standard Python type annotations and Pydantic models.

FastAPI does all the data validation, serialization, and documentation automatically using Pydantic underneath.

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    size: int = 0
    tags: set[str] = set()


@app.post("/items/")
def create_item(item: Item):
    return {"name": item.name, "size": item.size, "tags": item.tags}

A way to do this migration would be to start with a simple Pydantic model for a particular path operation, then passing it in the function parameters as in this example.

And then, when you see that the code accesses some field, e.g. name, go and add that field to the Pydantic model. If the code asumes that it will always be there, then make it a required field, if there's a default value, or the code in some way continues even when the field was not sent, then it probably isn't required, so you can put a default value in the Pydantic model (which makes it not required).

Then, in the place where that field is accessed, you pass instead the parameter attribute (item.name).

And in most of the cases, all that custom validation logic is just removed.

Tip

If you comment out the line that creates the main variable with the data, e.g.:

# data = request.get_json()

...then the editor will show you errors in each place where that variable is used, that will help you find the places that you need to update.

It's probably a good idea to comment out the code instead of removing it right away, in case you need to uncomment it to explore it or debug it further later.

Advanced Flask to FastAPI - Dependencies

Flask before_request

In Flask, there are several ways to handle things that should happen before the main code of an endpoint is called, probably one of the main ones would be app.before_request.

import secrets

from flask import Flask, jsonify, request, abort, g

internal_token = "thisshouldbeanenvironmentvariable"

app = Flask(__name__)


@app.before_request
def verify_token():
    token = request.headers.get("x-token", "")  # (1)!
    if not secrets.compare_digest(
        token.encode("utf-8"), internal_token.encode("utf-8")
    ):
        abort(401)
    g.token = token  # (2)!


@app.route("/items/", methods=["POST"])
def create_item():
    data = request.get_json()
    token = g.token  # (3)!
    return jsonify({"data": data, "token": token})
  1. In this example we extract a token from a header X-Token.

  2. Then the token is stored in Flask's own pseudo-global object g in an attribute.

  3. That value is extracted in another place in the code that reads from the same g object.

I'll tell you more about the usage of these pseudo-global objects from Flask later, but for now, let's see how that code would be changed in FastAPI.

FastAPI Dependencies

FastAPI has a simple dependency injection system, and it can be used to run code that extracts or generates data before the rest of the code executes.

You create a function that can declare all the same parameters that can be declared in a normal path operation function, with their types and default values, etc.

And then you tell FastAPI using the same path operation function parameters that this function depends on the value generated by a dependency.

import secrets

from fastapi import FastAPI, Header, HTTPException, Depends
from pydantic import BaseModel

internal_token = "thisshouldbeanenvironmentvariable"

app = FastAPI()


def verify_token(x_token: str = Header()):
    if not secrets.compare_digest(
        x_token.encode("utf-8"), internal_token.encode("utf-8")
    ):
        raise HTTPException(401)
    return x_token


class Item(BaseModel):
    name: str
    size: int = 0
    tags: set[str] = set()


@app.post("/items/")
def create_item(item: Item, token: str = Depends(verify_token)):
    return {"data": item, "token": token}

There are some key differences with Flask.

There's no use of global or pseudo-global objects. So, if you need to know where a value comes from, you can right-click in the dependency function and select "go to definition", to find where that data comes from.

And as dependency functions can declare anything that a path operation function can declare, they can also use other dependencies. And because it's all in the function parameters, FastAPI will extract all that information to automatically document the API, do data validation, and serialization.

Tip

In most of the cases when you feel you need something advanced, or you would use some type of Flask extension or plug-in, in FastAPI you probably just need a dependency function of 2 or 3 lines, that you normally just write yourself.

You normally wouldn't need extensions or plug-ins (that concept doesn't exist in FastAPI). Almost everything is covered with dependencies.

Testing

Testing is very similar in Flask and FastAPI.

In most of the cases you will be able to keep most of the code intact and only tweak little differences. But you will be able to test that your API is still behaving correctly quite easily with the same testing code.

Flask Testing

In Flask, you will probably have some setup with pytest.

You will probably have a pytest fixture to create the FlaskClient, and then use it to send requests to your app and test the responses.

import pytest
from flask.testing import FlaskClient

from .main import app


@pytest.fixture()
def client():
    return app.test_client()


def test_create_item(client: FlaskClient):
    response = client.post("/items/", json={"name": "Foo", "size": 3})
    assert response.json == {"data": {"name": "Foo", "size": 3}}

Tip

If you add the type annotation, like client: FlaskClient your editor will be able to provide you some autocompletion and inline errors.

FastAPI Testing

In FastAPI you would have something very similar, you can create the client directly or inside a fixture if that helps you in any way.

from fastapi.testclient import TestClient

from .main import app

client = TestClient(app)


def test_create_item():
    response = client.post("/items/", json={"name": "Foo", "size": 3})
    assert response.json() == {"data": {"name": "Foo", "size": 3}}

And if for some reason you need to use a pytest fixture, you can do it as well (even if it's just because all your functions expect the client from a fixture):

import pytest
from fastapi.testclient import TestClient

from .main import app


@pytest.fixture()
def client():
    with TestClient(app) as client:
        yield client


def test_create_item(client: TestClient):
    response = client.post("/items/", json={"name": "Foo", "size": 3})
    assert response.json() == {"data": {"name": "Foo", "size": 3}}

Config

Flask has the notion of an internal configuration system.

It is used for some internal configurations specific to Flask, but it's also used in many cases by extensions, and application code. This would be fine when assuming that the application code is used exclusively in Flask, but if you need to be able to use the same code in other places, for example in background tasks (e.g. with Celery or RQ), or when you need to re-use utilities on the FastAPI side, having configurations that depend on Flask doesn't work as well.

Config in Flask

For example, you could be loading some configs from files and environment variables:

flask_app.py
import json
import os

from flask import Flask, jsonify, request

app = Flask(__name__)

SECRET_KEY = os.getenv("SECRET_KEY")

app.config.from_file("config.json", load=json.load)

app.config.update(
    SECRET_KEY=SECRET_KEY
)


@app.route("/items/", methods=["POST"])
def create_item():
    data = request.get_json()
    return jsonify({"data": data})

Then you could extract the configuration values from the dictionary-like object at app.config, for example:

secret_key = app.config["SECRET_KEY"]

Config in FastAPI and Others

FastAPI doesn't have an internal configuration system to store data like Flask. Instead, as FastAPI is based on Pydantic, it encourages you to use Pydantic's own settings management.

For example, you could have a file just with configurations/settings:

config.py
from functools import lru_cache

from pydantic import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50

    class Config:
        env_file = ".env"


@lru_cache()
def get_settings():
    return Settings()

This would read a file .env with environment-like variables in it. And those configs would be automatically overriden by actual environment variables.

Creating an instance of this class would read a file, and you probably don't want to do that every time you want to access the config values, as reading a file from disk is slower than accessing a variable in memory. To avoid doing that, we create a function get_settings() that creates and returns the instance, but by decorating it with @lru_cache(), it will run the first time, and the next times it will just return the same value instead of creating a new instance.

Tip

Learn more details about this on the FastAPI docs and the Pydantic docs.

Then you can use that config and that utility function in your FastAPI app:

fastapi_app.py
from fastapi import Depends, FastAPI

from .config import Settings, get_settings

app = FastAPI()


@app.get("/info")
async def info(settings: Settings = Depends(get_settings)):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

Here we use it in a dependency. That would allow overriding the settings during testing.

Config in Flask plus FastAPI

For many things, in particular internal configs in Flask, you will still need to use the standard Flask configs.

But for other settings and configs that are more general and not specific to Flask, you could put them in Pydantic settings, and then re-use those utilities in other places, in the code related to your app in Flask (but that is not Flask itself), and also in the FastAPI code.

You will probably still have to duplicate some of those configs to put them in the Flask-specific config system (for example the secrets used by Flask) and the Pydantic settings system for the rest of your code, but you can minimize the amount of things you have to duplicate.

Migrate Flask Extensions

Flask has the notion of extensions, in many cases having extensions is necessary, in particular because of Flask's design using pseudo-global objects like request.

There are also cases where some extensions provide some conveniences on top of other libraries, but sometimes those extensions are not required to use the underlying library. And in cases it could be simpler to use the underlying library directly instead.

Let's say you have some code that uses those libraries, and you want to use it also outside of the Flask-specific code, it would probably make sense to use the libraries directly instead of using the extension. Because otherwise you would be depending on somehow having a Flask application around in a scenario where it doesn't make sense, for example, in an RQ job, or a FastAPI app.

Moving away from the extensions to use the library directly can be a lot easier than it seems at first. For example, moving from Flask-SQLAlchemy to pure SQLAlchemy normally just involves inheriting from a SQLAlchemy base class instead of the one provided by Flask-SQLAlchemy, and creating a session directly. Or you could even use Flask-SQLAlchemy's session for the Flask parts, and use a session created directly in the rest of the code. This way, you can use pure SQLAlchemy models in Flask but also in RQ, FastAPI or anything else you use.

Another example is Flask-MongoEngine. One of the main things it does is start the mongoengine connection. That's something that you can do manually on startup too, it's normally just the line:

mongoengine.connect(**MONGO_ENGINE_SETTINGS)

Reducing the number of extensions that you use, in particular for things that are not directly related to Flask's functionality, will allow you to re-use those parts in other places, and share the code more easily, or migrate gradually.

Recap

Here you saw all the main changes needed to migrate from Flask to FastAPI.

For most simple applications this would be enough.

If you have a complex app with some advanced patterns (e.g. using the g pseudo-global object in your code base), then the next blog post will cover some of the advanced techniques you can apply to migrate from Flask to FastAPI.