Chapter 01
Python essentials for the backend
How Python runs web code, how async and the GIL behave, and how projects, types and frameworks are set up.
async/await and ASGI
Also called ASGI, WSGI, asyncio, Uvicorn
- What
- ASGI is the async interface between a Python web app and its server (Uvicorn, Granian).
async defendpoints run on the event loop; FastAPI runs plaindefendpoints in a thread pool. - Why
- Async lets one worker wait on many slow network calls at once. WSGI, the older sync interface, handles one request per thread or process.
@app.get("/report")
async def report():
await asyncio.sleep(1) # other requests keep running
return {"ok": True}
@app.get("/legacy")
def legacy():
time.sleep(1) # fine: FastAPI runs def in a thread pool
return {"ok": True}Watch out
Calling blocking code (requests, time.sleep, a sync DB driver) inside async def freezes every request on that worker.
The GIL
Also called Global Interpreter Lock, free-threaded Python
- What
- In the standard CPython build, the Global Interpreter Lock lets only one thread execute Python bytecode at a time. Threads still help for I/O, because waiting releases the lock. A free-threaded build without the GIL is supported but not the default.
- Why
- It decides how you scale: async or threads for I/O-bound work, multiple processes (Gunicorn or Uvicorn workers, Celery) for CPU-bound work.
- Example
- A FastAPI app resizing images in request handlers uses one core however many threads it has; running 4 worker processes or pushing resizing to a Celery queue uses all 4 cores.
Watch out
Adding threads to speed up CPU-heavy Python code and seeing no gain.
Virtual environments, pip and uv
Also called venv, pyproject.toml, uv, pip, virtual environment
- What
- A virtual environment is an isolated folder of packages for one project.
pyproject.tomldeclares dependencies; uv (or pip with a lockfile) installs them reproducibly. - Why
- Projects need different versions of the same library. Isolation and a lockfile keep your laptop, CI and production in step.
uv init shop-api && cd shop-api
uv add "fastapi[standard]" "psycopg[binary]"
uv run fastapi dev app/main.py
# the classic way
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txtWatch out
Installing into the system Python, then wondering why production has different versions.
Type hints
Also called typing, mypy, pyright
- What
- Annotations that describe the types of variables, parameters and return values. Python 3.12 adds
typealiases and generic syntax likedef first[T]. - Why
- Editors and checkers (mypy, pyright) catch bugs before runtime, and FastAPI and Pydantic read hints to validate and document requests.
type UserId = int
def find_user(user_id: UserId) -> User | None: ...
def first[T](items: list[T]) -> T | None:
return items[0] if items else NoneWatch out
Assuming hints are enforced: plain Python ignores them at runtime unless a library like Pydantic checks them.
Django URLs, views and apps
Also called Django, DRF
- What
- Django maps a URL pattern to a view function or class, which uses models (the ORM) and returns a response. Code is split into apps, each with models, views and migrations.
- Why
- It ships auth, admin, ORM, migrations and forms in one framework. Django REST Framework adds serializers and viewsets for JSON APIs.
# urls.py
urlpatterns = [path("users/<int:pk>/", views.user_detail)]
# views.py
def user_detail(request, pk):
user = get_object_or_404(User, pk=pk)
return JsonResponse({"id": user.pk, "email": user.email})Watch out
Putting business logic in views; keep it in models or service functions so it can be tested and reused.
Python core: types, collections, comprehensions
Also called list, dict, comprehension
- What
- Built-in list, dict, set and tuple cover most data handling, and comprehensions build new collections in one readable line. Names are references to objects, so two names can point at the same list.
- Why
- Request data, query results and config are all dicts and lists, so fluent collection code is most of everyday backend Python.
orders = [{"id": 1, "total": 40}, {"id": 2, "total": 90}]
big = [o["id"] for o in orders if o["total"] > 50]
by_id = {o["id"]: o for o in orders}
tags = set(["api", "db", "api"]) # {'api', 'db'}
def add_tag(tag, tags=None): # never tags=[]
tags = tags or []
tags.append(tag)
return tagsWatch out
A mutable default argument such as def f(x=[]), which is shared between calls.
Python classes and exceptions
Also called class, try except, raise
- What
- Classes group data and behaviour, and dataclasses remove boilerplate for plain data. Errors are raised as exceptions and handled with try and except, catching the narrowest type you can.
- Why
- Services and models in Django and FastAPI are built from these, and custom exceptions let one handler map each failure to the right status code.
class OrderNotFound(Exception):
"""Raised when an order id does not exist."""
def get_order(orders: dict, order_id: int):
try:
return orders[order_id]
except KeyError:
raise OrderNotFound(order_id) from None
try:
get_order({}, 7)
except OrderNotFound as e:
print("404 for order", e)Watch out
A bare except: that swallows every error, including typos and Ctrl+C.