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.
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 declares | What FastAPI does with it |
|---|---|
invoice_id: int in the path | Converts the URL fragment into an integer, refuses the rest with a 422 error |
limit: int = 20 | Optional query parameter, default value shown in the documentation |
invoice: Invoice | Reads 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.
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.
| Need | Flask | Django | FastAPI |
|---|---|---|---|
| Serving HTML pages | With a template engine | Its home ground | Possible, but beside the point |
| Validating incoming data | Written or bolted on | Forms and serialisers | Included, drawn from annotations |
| Interactive documentation | Extension to install | Extension to install | Generated without a line |
| Database, admin, accounts | To assemble | Provided | To assemble |
| Slow calls in parallel | One thread per call | One thread per call | Event 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.
# 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.
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.
pip install fastapi uvicorn
uvicorn main:app --reloadWhen 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
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.
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.
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.