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 than
    • lt Less than
    • ge Greater than or equal to
    • le Less than or equal to
    • multiple_of An integer multiple of a value
  • String constr:
    • min_length (character length)
    • max_length
    • to_upper Convert to uppercase
    • to_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 resource
  • verb /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 code
    • web contains the FastAPI web layer
    • service contains the business logic layer
    • data contains the storage interface layer
    • model contains Pydantic model definitions
    • fake (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, alongside web, service, data, and model
    • 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).

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.