Notes on
FastAPI: Modern Python Web Development
by Bill Lubanovic
| 11 min read
My code for this book: GitHub - chhoumann/fastapi-book.
Part I. What’s New?
Chapter 1. The Modern Web
Services and APIs
REST(ful)
Roy Fielding’s Ph.D. thesis defined REST as an architectural style for HTTP use. It’s often misunderstood.
Here’s the rough shared adaptation that dominates the web.
It’s called RESTful, with these characteristics:
- Uses HTTP and client-server protocol
- Stateless (each connection is independent)
- Cacheable
- Resource-based
A resource is data that you can distinguish and perform actions on.
A web service provides an endpoint—a distinct URL and HTTP verb (action)—for each feature it wants to expose. Endpoints are also called routes, as they route the URL to a function.
A client sends a request to a RESTful endpoint with data in one of the following areas of an HTTP message:
- Headers
- The URL string
- Query parameters
- Body values
And an HTTP response returns an integer status code, various headers, and a body (can be empty, single, or chunked in successive pieces).
Concurrency
Concurrency does not mean full parallelism; multiple processes do not occur simultaneously on a single CPU.
Instead, concurrency mostly avoids busy waiting, meaning idling the CPU until a response is delivered.
Networks and disks are slow compared to CPUs, so whenever we talk to them, we don’t just want to sit and wait until they’ve responded.
While normal Python execution is synchronous, sometimes we want to be asynchronous, meaning we want to a little of something, then a little of another, then back to the first thing, and so on with many things.
If all our code is CPU bound (all of it uses the CPU to compute stuff), there’s no time to be asynchronous. But if we do things that make the CPU wait for an external thing to complete (I/O bound), we can be asynchronous.
Asynchronous systems provide an event loop, wherein requests for slow operations are sent and noted, but we don’t actively wait for them to respond. Instead, we do immediate processing on each pass through the loop, and any responses that came in during that time are handled in the next pass.
Chapter 3. Modern Python
Web Frameworks
Web Server Gateway Interface (WSGI) is a synchronous Python standard specification to connect application code to web servers.
Many traditional Python web frameworks are built on WSGI.
But synchronous communication may mean busy waiting. Because of this, the Python Asynchronous Server Gateway Interface (ASGI) specification was developed.
A few frameworks:
- Django (2003)
- Flask (2010)
- FastAPI (2018)
FastAPI is rapidly gaining popularity, and may overtake the other two.
Sebastián Ramírez, the author of FastAPI, came up with a design that was heavily based on Starlette for web details and Pydantic for data details.
Part II. A FastAPI Tour
Chapter 3. FastAPI Tour
FastAPI is as fast as Node.js and Go in some cases.
HTTP Requests
from fastapi import FastAPI
app = FastAPI()
@app.get("/hi")
def greet():
return "Hello? World?"
@app.get("/hi")
is a path decorator telling FastAPI that requests for /hi
on the server should be directed to the function it’s decorating, and that the decorator only applies to the HTTP GET
verb. And def greet()
is a path function.
FastAPI does not include a web server but recommends using Uvicorn.
URL path
@app.get("/hi/{who}")
def greet(who):
return f"Hello? {who}?"
Query parameters
@app.get("/hi")
def greet(who):
return f"Hello? {who}?"
Since who
isn’t in the URL, FastAPI assumes it’s a query parameter.
Body
@app.post("/hi")
def greet(who: str = Body(embed=True)):
return f"Hello? {who}?"
Body(embed=True)
is to tell FastAPI that, this time, we get the value of who
from the JSON-formatted request body. embed
means that it should look like {"who": "You"}
rather than just "You"
.
HTTP header
@app.post("/hi")
def greet(who: str = Header()):
return f"Hello? {who}?"
FastAPI converts HTTP header keys to lowercase, and converts a hyphen to an underscore. So User-Agent
becomes user_agent
.
HTTP Responses
By default, FastAPI converts whatever you return from your endpoint function to JSON. And puts the header line Content-Type: application/json
in your response.
You can set the default status code via the decorator:
@app.post("/items/", status_code=201)
async def create_item(name: str):
return {"name": name}
You can inject HTTP response headers:
from fastapi import Response
@app.get("/header/{name}/{value}")
def header(name: str, value: str, response: Response):
response.headers[name] = value
return "body"
Response types (fastapi.responses
):
- JSONResponse (default)
- HTMLResponse
- PlainTextResponse
- RedirectResponse
- FileResponse
- StreamingResponse
By default, FastAPI takes whatever you’re returning from the path function, converts it to a JSON string and returns it, with the matching HTTP response headers Content-Length
and Content-Type
. This also works for any Pydantic model class.
You can use different classes with many of the fields, and have them specialised for different cases: e.g. user input, for responses, and for internal use. You may, for example, want to remove certain sensitive information from the output, or add fields to the user input (creation date, time, etc.).
from datetime import datetime
from pydantic import BaseClass
import service.tag as service
class TagIn(Baseclass):
tag: str
class Tag(BaseClass):
tag: str
created: datetime
secret: str
class TagOut(BaseClass):
tag: str
created: datetime
@app.post('/')
def create(tag_in: TagIn) -> TagIn:
tag: Tag = Tag(
tag=tag_in.tag,
created=datetime.utcnow(),
secret="very secret"
)
service.create(tag)
return tag_in
@app.get('/{tag_str}', response_model=TagOut)
def get_one(tag_str: str) -> TagOut:
tag: Tag = service.get(tag_str)
# Returns only the `Tag` model properties
return tag
Chapter 4. Async, Concurrency, and Starlette Tour
Types of Concurrency
Parallel computing: task is spread across multiple dedicated CPUs at the same time.
Concurrent computing: each CPU switches among multiple tasks.
Distributed and Parallel Computing
Running your application as separate pieces on separate CPUs in a single machine or on multiple machines.
There are many ways to do this.
Operating System Processes
Using multiple OS processes to run your apps.
See the multiprocessing
module in Python.
Operating System Threads
Can run threads of control within a single process. See threading
package.
Threads are recommended when your program is I/O bound.
Processes are recommended when you’re CPU bound.
concurrent.futures
makes it easier.
async
functions help. FastAPI manages threads for normal synchronous functions via threadpools.
Green Threads
Green threads, presented by greenlet, gevent, Eventlet. These are cooperative, not preemptive. Runs in user space (your program) rather than in the OS kernel.
Works by monkey-patching standard Python functions to make concurrent code look like normal sequential code: give up control when they would block waiting for I/O.
OS threads are lighter than OS processes, and green threads are lighter than OS threads.
Callbacks
Package: Twisted.
Python Generators
Use yield
.
FastAPI and Async
Web servers usually spend a lot of time waiting, so performance can be increased by avoiding some of that waiting. We can do that with concurrency.
Other web servers use e.g. threads, gevent, etc.
FastAPI is one of the fastest Python web frameworks because it uses async code, leveraging the underlying Starlette package’s ASGI support and its own innovations.
Chapter 5. Pydantic, Type Hints, and Models Tour
Dataclasses in Python are somewhat analogous to what other languags call records or structs.
from dataclasses import dataclass
@dataclass
class CreatureDataClass():
name: str
country: str
area: str
description: str
aka: str
FastAPI uses Pydantic, so just use that.
Pydantic has great validation, and its integration with FastAPI helps catch many potential errors.
A great benefit with Pydantic is that it uses standard Python type hints—many previous libraries predated type hints and therefore rolled their own.
from pydantic import BaseModel
class Creature(BaseModel):
name: str
country: str
area: str
description: str
aka: str
Validate Values
- Integer
conint
or float:gt
Greater thanlt
Less thange
Greater than or equal tole
Less than or equal tomultiple_of
An integer multiple of a value
- String
constr
:min_length
(character length)max_length
to_upper
Convert to uppercaseto_lower
regex
Match Python regular expression
- Tuple, list, or set
min_items
max_items
It goes without saying that FastAPI updates over time and that these can be deprecated / switched out. Technical content in books is not evergreen.
You can also use Field
.
class Creature(BaseModel):
name: str = constr(min_length=1)
country: str = Field(..., min_length=2)
area: str
description: str
aka: str
...
as an argument to Field()
means the value is required, and that there’s no default value.
Chapter 6. Dependencies
For dependency injection.
A dependency is specific information that you need at some point.
Just grabbing what you need, whenever you need it, can work, but there are consequences of doing so.
It becomes harder to test variations of the function. You’re adding hidden dependencies. It can lead to code duplication. And so on.
With dependency injection, you can pass any specific information that your function needs into the function. The traditional way is to pass a helper function, e.g. get_database_client
rather than just importing and using a global database_client
.
With FastAPI, you can define dependencies as arguments to your functions, and they’re automatically called by FastAPI, which passes in the values the functions return.
Some common examples: Path
, Query
, Body
, and Header
are all dependencies. These are functions or Python classes that dig the request data from various areas in the HTTP request, hiding the details of e.g. validity checks and data formats.
In FastAPI, dependencies are to be executed, so the dependency object should be of type Callable
(includes functions and classes).
The dependencies you define are like FastAPI path functions, meaning it knows about things like Params
, but it doesn’t have a path decorator above it—it’s just a helper, not an endpoint.
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
return {"q": q, "skip": skip, "limit": limit}
@app.get("/items/")
async def read_items(commons: Annotated[dict, Depends(common_parameters)]):
return commons
# Any path under `router` now gets this dependency
router = APIRouter(dependencies=[Depends(common_parameters)])
Similarly, you can define it globally—for all routes.
Chapter 7. Framework Comparisons
Neither FastAPI nor Flask has built-in database handling functionalities like Django.
SQLAlchemy is great.
The author of FastAPI has used SQLAlchemy and Pydantic to create SQLModel.
Frameworks:
- FastAPI
- Flask
- Django
- Bottle (minimal)
- Litestar (similar to FastAPI, based on ASGI/Starlette and Pydantic)
- AIOHTTP (ASGI client and server)
- Socketify.py (new, claims high performance)
Part III. Making a Website
Chapter 8. Web Layer
RESTful API Design
Core components of RESTful designs:
- Resources: the data elements your application manages
- IDs: unique resource identifiers
- URLs: structured resource and ID strings
- Verbs or actions: terms that accompany URLs for different purposes (
GET
,POST
, …)
Generally, you combine verbs and URLs containing resources and IDs using these patterns of path parameters:
verb /resource/
to apply verb to all resource of type resourceverb /resource/id
to apply verb to the resource with ID id
Web requests often carry more information, indicating to:
- Sort results
- Paginate results
- Perform another function (e.g. filtering, …)
These can be expressed as path parameters, but are often included as query parameters.
URLs have size limits, so large requests are often conveyed in the HTTP body.
Most authors recommend using plurals when naming the resource, and related namespaces like API sections and database tables.
But the author of this book (now) thinks that the singular names are simpler, so that’s what’s used in this book.
File and Directory Site Layout
src
contains all website codeweb
contains the FastAPI web layerservice
contains the business logic layerdata
contains the storage interface layermodel
contains Pydantic model definitionsfake
(hardwired stub data used for testing in this book)
In each folder is a __init__.py
file, which is needed to treat the directory as a package.
The __init__.py
files are empty. It’s like a Python hack such that the directory is treated as a Python package that may be imported from.
Chapter 9. Service Layer
Let’s start with creature.py. At this point, the needs of explorer.py will be almost the same, and we can borrow almost everything. It’s so tempting to write a single service file that handles both, but, almost inevitably, at some point we’ll need to handle them differently
It’s always tempting to over-abstract. Don’t!
Test!
Directory structure:
test
top-level directory, alongsideweb
,service
,data
, andmodel
unit
web
service
data
full
also known as end-to-end or contract tests
Files in these folders have the test_
prefix or _test
suffix for use by pytest.
Chapter 13. Production
When deploying applications you will probably want to have some replication of processes to take advantage of multiple cores and to be able to handle more requests.
from Server Workers - Uvicorn with Workers - FastAPI.
Use HTTPS: About HTTPS - FastAPI.
Docker can make it easy to deploy: FastAPI in Containers - Docker - FastAPI.
If you have an endpoint that gets data from a static source, consider caching it. You can use e.g. cache
or lru_cache
from functools
.
Put indexes on your database. Use a query optimizer.
If you’re performing tasks that take longer than a fraction of a second (e.g., sending a confirmation email, downsizing images), consider handing off the task to a job queue like Celery.
If your web service seems slow because it does a lot with Python, consider using a “faster Python”:
- PyPy instead of the standard CPython
- Write a python extension in C, C++, or Rust
- Convert slow Python code to Cython
Or Mojo (in the future, maybe).
Part IV. A Gallery
Chapter 14. Databases, Data Science, and a Little AI
SQLAlchemy is great. It’s more than just an ORM.
SQLModel is also great.
Chapter 15. Files
Use File()
for small files and UploadFile
for larger files.
Download files from the server with StreamingResponse
(returns the file in chunks).
You can serve static files with StaticFiles
.
Liked these notes? Join the newsletter.
Get notified whenever I post new notes.