FastAPI: building a fast web interface in Python

FastAPI exposes Python functions over the network and derives both data validation and interactive documentation straight from the type annotations.
7 min read
Believemy logo

Definition

You have written a Python function that does its job well: it works out an amount, it records an invoice. The day someone else needs to use it, a web interface or a neighbouring service, the difficulty is no longer the calculation. You have to pick an address, read what arrives over the network, check that the incoming data has the expected shape, hand back a readable answer, then explain all of it to whoever will be calling. That plumbing often takes more lines than the function itself.

FastAPI is the library that takes it on. It makes Python functions callable from the outside without you writing by hand the HTTP layer that separates them from the world. Your business code does not move, and it stays testable without any server starting up.

PYTHON
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

# The expected shape of the incoming data, described just once
class Invoice(BaseModel):
    client: str
    amount: float

@app.post("/invoices")
def record(invoice: Invoice):
    return {"client": invoice.client, "gross": round(invoice.amount * 1.2, 2)}

Three things happen here without anything asking for them. The address /invoices exists and accepts a data submission. An amount spelled out in words is refused before it even enters the function, with a message naming the offending field. And interactive documentation appears on /docs, where every route can be read and tried from the browser.

All it took was an ordinary function, a decorator above it and an answer returned as json. That documentation is not a gimmick: it is the contract read by whoever consumes your interface, and nobody had to write it or keep it up to date.


Annotations stop being decorative

What remains is to understand where the tool draws all of this from, since you declared nothing beyond types. This is the only genuinely new idea in the library.

Everywhere else in Python, a type hint documents and feeds a checker such as mypy, but it has no effect while the program runs: you can announce amount: float, pass a string, and nothing protests. FastAPI turns the annotation into an instruction. What you used to write for your colleagues becomes what drives the work.

Here, one line at a time, is what the library infers from a function signature.

What the signature declaresWhat FastAPI does with it
invoice_id: int in the pathConverts the URL fragment into an integer, refuses the rest with a 422 error
limit: int = 20Optional query parameter, default value shown in the documentation
invoice: InvoiceReads the request body, validates every field, lists the missing ones
-> list[Invoice]Filters the response down to the declared fields only

The validation itself does not come from FastAPI but from Pydantic, a separate library it has made its engine. A model inheriting from the base class describes the expected shape of the data, and that description serves three times over: it converts, it refuses, it documents.

The benefit shows in the code that disappears rather than the code that is added: the fifteen lines of manual checking at the top of every route, and the documentation file kept on the side, the one that was already lying by the second week for want of being reopened.

Good to know

Those same declarations produce a description in the OpenAPI format, which other tools know how to read. From there, client code is generated in another language without a single line written by hand.


Against Flask and Django

The next question is almost always the same: why this one rather than another. It has no absolute answer, because the three tools are not aiming at the same object. Look in the table for the line that describes your project.

NeedFlaskDjangoFastAPI
Serving HTML pagesWith a template engineIts home groundPossible, but beside the point
Validating incoming dataWritten or bolted onForms and serialisersIncluded, drawn from annotations
Interactive documentationExtension to installExtension to installGenerated without a line
Database, admin, accountsTo assembleProvidedTo assemble
Slow calls in parallelOne thread per callOne thread per callEvent loop

The split sums up rather well. Django suits a complete site, with its pages, its administration and its accounts, because it provides all of that from day one. Flask suits a fifty-line service whose documentation nobody will ever read. FastAPI takes the lead as soon as other programs consume your data and have to be told, precisely, what you accept and what you hand back.


The first-days mistake

One trap is waiting for just about everyone, and it costs all the more because it triggers no error message. It concerns async.

A route declared asynchronous does not go onto its own thread: it runs inside the event loop, and there is only one of those for the whole server. The smallest blocking call placed in it freezes the entire service, for every visitor at once. Alone in front of your machine, you will never see a thing.

PYTHON
# Avoid: this call blocks, and there is only one event loop
@app.get("/rates")
async def rates():
    return requests.get("https://api.example.com/rates").json()

# Correct: without async, the route goes onto a separate thread
@app.get("/rates")
def rates():
    return requests.get("https://api.example.com/rates").json()

The rule holds in two steps. A function written normally, without async, goes onto a separate thread and bothers nobody: that is the default choice, and it is a good one. An asynchronous function must only call code built for it, with await in front of every wait.

Warning

Calling requests from an asynchronous route is the most widespread mistake. Everything works on your machine, then the server collapses at three simultaneous users in production.

The second surprise arrives on the same day: FastAPI is not a server. Running the file through the interpreter launches nothing and the page stays unreachable. An ASGI server is needed to carry the application, uvicorn in the vast majority of cases, installed in the same virtual environment as the library.

BASH
pip install fastapi uvicorn
uvicorn main:app --reload


When it is not worth it

Two situations come up where it is better left aside.

The first is the script triggered by a scheduled task, reading one file and writing its result next to it. Adding an HTTP interface so it can call itself means one more port to watch, one more process to restart and one more attack surface, in exchange for nothing.

The second is the content site, with its pages, its contact form and its administration area. What a complete framework provides from installation would represent weeks of rebuilding here, for a less solid result.

What remains is knowing what the library does not contain, since the question comes up once the tutorial is over: no database, no migrations, no administration interface, no account handling. Those are separate pieces, to be assembled yourself. The stance is deliberate, each piece being replaceable without touching the rest, but it moves the work rather than removing it.


Frequently asked questions

Question

Should every route be written as asynchronous?

No, and that is the most widespread misreading. An ordinary route is perfectly supported: FastAPI runs it on a separate thread, with no risk of blocking whatsoever. Keep the asynchronous form for routes that genuinely call libraries built for it, otherwise you take on all of the risk without ever touching the benefit.

Question

Is it really faster than the others?

On a route that computes, no: it is the same Python interpreter, running at the same speed. The gap appears on routes that wait for something outside, a database or a remote service, because the event loop serves other calls during the wait instead of tying up a thread. A service doing nothing but computation will gain absolutely nothing.

Question

Is the generated documentation usable as it stands?

Yes, and that is often what settles the decision. It describes every route, every field and every response code, it can be tried straight from the browser, and it stays accurate since it is produced from the code itself. You enrich it by writing the docstring of each function, which becomes its on-screen description.

Related terms

Discover our python glossary

Browse the terms and definitions most commonly used in development with Python.

Share this article

Want to help us? Share this article on your networks or even better: on your site, in an article or in your newsletter.