FastAPI Mastery Course
Topic 1 / 22  ·  Python Foundations
Topic 1 · Section 1.1
Python Runtime
Before building FastAPI apps, you need a rock-solid understanding of how Python actually runs your code — from source file to bytecode to execution. This foundation explains why FastAPI makes the architectural decisions it does.
1.1.1 CPython — The Reference Interpreter
🏗️ Interpreter Architecture
CPython is the official, most-used Python implementation, written in C. When you run python app.py, CPython is doing all the work. Understanding its architecture helps you write faster, more efficient FastAPI code.

The pipeline: Source code (.py)Tokenizer/ParserASTBytecode compiler.pyc filePython VM executes bytecode
💡
FastAPI relevance: CPython's GIL (Global Interpreter Lock) means only one thread runs Python at a time. This is WHY FastAPI uses async/await — to achieve concurrency without threads.
python
# You can inspect CPython's compilation pipeline yourself
import dis
import py_compile

# 1. Write a simple function
def add(a, b):
    return a + b

# 2. Disassemble: see the bytecode CPython generates
dis.dis(add)
# Output shows bytecode instructions like:
#   LOAD_FAST    0 (a)
#   LOAD_FAST    1 (b)
#   BINARY_OP    0 (+)
#   RETURN_VALUE

# 3. Inspect the code object CPython creates
print(add.__code__.co_consts)   # constants
print(add.__code__.co_varnames) # local variable names
print(add.__code__.co_code)     # raw bytecode bytes
🔄 Compilation Pipeline & Bytecode
CPython does NOT compile to native machine code. It compiles to bytecode — a compact, platform-independent set of instructions for the Python Virtual Machine. Bytecode is cached in __pycache__/ as .pyc files to speed up future runs.
python
import marshal, dis

# Read a compiled .pyc file and inspect its bytecode
with open('__pycache__/mymodule.cpython-312.pyc', 'rb') as f:
    f.read(16)               # skip magic + metadata
    code = marshal.load(f)  # load the bytecode object

dis.dis(code)  # disassemble — shows bytecode instructions

# Python also gives you the AST (Abstract Syntax Tree)
import ast
tree = ast.parse("x = 1 + 2")
print(ast.dump(tree, indent=2))
# Module(body=[Assign(targets=[Name(id='x')],
#         value=BinOp(left=Constant(1), op=Add(), right=Constant(2)))])
🖥️ Virtual Machine (PVM)
The Python Virtual Machine is a stack-based interpreter that executes bytecode one instruction at a time. It maintains an evaluation stack where operands are pushed/popped. It also manages frames — one per function call.
⚠️
GIL (Global Interpreter Lock): The PVM allows only ONE thread to execute Python bytecode at a time. This is why CPU-bound multithreading doesn't give speedup in Python. FastAPI solves I/O bottlenecks using async/await, not threads.
python
import sys

# Each function call creates a "frame" on the call stack
def inner():
    frame = sys._getframe()        # current frame
    print(frame.f_code.co_name)    # 'inner'
    print(frame.f_back.f_code.co_name)  # caller's name

def outer():
    inner()

outer()

# sys.getrecursionlimit() — PVM enforces max stack depth
print(sys.getrecursionlimit())  # 1000 by default

1.1.2 Execution Model
📦 Module Loading & Imports
When Python imports a module, it: (1) searches sys.path, (2) compiles to bytecode if needed, (3) executes the module at top-level, (4) stores it in sys.modules for caching. Second imports reuse the cached version.
FastAPI: Module-level code runs once at startup. This is where you initialize your database engines, settings, and shared resources — not inside request handlers.
python
# module: myapp/database.py
print("database.py is being executed")  # runs ONCE
engine = create_engine("sqlite:///app.db")  # created ONCE

# main.py
import myapp.database  # executes database.py → prints message
import myapp.database  # CACHED — does NOT execute again

# Inspect what's loaded
import sys
print('myapp.database' in sys.modules)  # True

# sys.path controls where Python looks for modules
print(sys.path)  # list of directories to search
🔍 Name Resolution & LEGB Rule
Python looks up variable names in this order: Local → Enclosing → Global → Built-in. Understanding LEGB is critical for writing correct FastAPI dependency functions and middleware.
python
# LEGB in action
x = "global"               # Global scope

def outer():
    x = "enclosing"         # Enclosing scope

    def inner():
        x = "local"          # Local scope
        print(x)              # → "local"  (L wins)

    def inner2():
        print(x)              # → "enclosing" (E, since no local x)

    inner()
    inner2()

outer()
print(x)                    # → "global"  (G)
print(len)                  # → built-in function (B)

# Modifying outer scope: use 'global' or 'nonlocal'
counter = 0
def increment():
    global counter           # explicitly target global
    counter += 1

increment()
print(counter)  # → 1
🔒 Closures
A closure is a function that "remembers" variables from its enclosing scope even after that scope has finished executing. FastAPI uses closures extensively in dependency injection and middleware factories.
python
# Basic closure
def make_multiplier(factor):
    def multiply(x):
        return x * factor      # 'factor' is a free variable
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5))  # → 10
print(triple(5)) # → 15

# Inspect the closure's captured variables
print(double.__closure__[0].cell_contents)  # → 2

# ── FastAPI real-world pattern ──
def require_role(role: str):
    """Returns a FastAPI dependency that checks for a specific role"""
    async def dependency(current_user = Depends(get_current_user)):
        if role not in current_user.roles:
            raise HTTPException(status_code=403)
        return current_user
    return dependency  # 'role' is captured in closure

# Usage:
# @app.get("/admin", dependencies=[Depends(require_role("admin"))])

1.1.3 Memory Management
📊 Stack vs Heap Memory
In Python: Stack holds call frames (local variables, function calls) — fast, automatically managed. Heap holds all Python objects (lists, dicts, class instances) — managed by CPython's memory allocator and garbage collector.
Stack
  • Function call frames
  • Local variable names
  • References (pointers)
  • Auto-cleaned on return
Heap
  • All Python objects
  • Lists, dicts, instances
  • Strings, numbers
  • Managed by GC
🔢 Reference Counting
Every Python object has a reference count. When you assign an object to a variable, the count goes up. When the variable goes out of scope or is reassigned, the count goes down. When it hits zero, the memory is freed immediately.
python
import sys

# Reference counting demo
a = []           # create list object → refcount = 1
b = a            # another reference → refcount = 2
print(sys.getrefcount(a))  # → 3 (a, b, and getrefcount's arg)

c = [a, a]       # list holding 2 refs to same object
print(sys.getrefcount(a))  # → 5

del b            # refcount drops by 1
del c            # refcount drops by 2
print(sys.getrefcount(a))  # → 2 (back to a + getrefcount)

# Object identity vs equality
x = "hello"
y = "hello"
print(x is y)    # Often True — string interning!
print(id(x))     # memory address of x
print(id(y))     # same address — same object
♻️ Cyclic Garbage Collection
Reference counting fails for circular references (A → B → A). Python's cyclic GC runs periodically to detect and collect these cycles. It uses generational collection — younger objects are collected more frequently since most objects die young.
⚠️
FastAPI caution: Avoid circular references in your models and service objects. They delay GC, and in high-traffic APIs this accumulates memory pressure.
python
import gc

# Create a circular reference
class Node:
    def __init__(self):
        self.ref = None

a = Node()
b = Node()
a.ref = b    # a → b
b.ref = a    # b → a  (cycle!)

del a
del b
# refcount of both is 1 (due to cycle), NOT freed by refcount

gc.collect()   # manually trigger cyclic GC

# Check GC generations
print(gc.get_threshold())  # (700, 10, 10) — gen0, gen1, gen2
print(gc.get_count())      # objects in each generation

# Object lifecycle summary:
# __new__  → allocate memory
# __init__ → initialize fields
# __del__  → called before deallocation (avoid relying on this!)
Topic 1 · Section 1.2
Object-Oriented Programming
FastAPI is deeply object-oriented — from Pydantic models to dependency injection classes. Mastering Python OOP lets you write modular, reusable, and testable FastAPI applications.
1.2.1 Classes
📐 Class Definition, Attributes & Methods
A class is a blueprint for creating objects. It defines attributes (data) and methods (behavior). In FastAPI, classes are used for dependency injection, service layers, and Pydantic models.
python
class UserService:
    # Class attribute (shared across all instances)
    service_name: str = "UserService"

    # Constructor — called when you do UserService(...)
    def __init__(self, db_url: str):
        # Instance attributes (unique per instance)
        self.db_url = db_url
        self._users: list = []          # _ prefix = convention for "private"
        self.__secret = "hidden"        # __ prefix = name mangling

    # Instance method
    def get_user(self, user_id: int):
        return next((u for u in self._users if u['id'] == user_id), None)

    # Class method — receives the CLASS not instance
    @classmethod
    def from_env(cls):
        import os
        return cls(db_url=os.getenv("DATABASE_URL"))

    # Static method — no access to class or instance
    @staticmethod
    def validate_email(email: str) -> bool:
        return "@" in email

    # __repr__ and __str__ for debugging
    def __repr__(self):
        return f"UserService(db_url={self.db_url!r})"

svc = UserService("postgresql://localhost/app")
print(svc.validate_email("user@example.com"))  # True
svc2 = UserService.from_env()  # factory pattern

1.2.2 Inheritance
🌳 Single & Multiple Inheritance + MRO
Python supports single (one parent) and multiple (many parents) inheritance. The MRO (Method Resolution Order) — computed via the C3 linearization algorithm — determines which method is called when multiple parents define the same name.
python
# Single Inheritance
class BaseRepository:
    def __init__(self, db):
        self.db = db

    def find_by_id(self, id: int):
        raise NotImplementedError

class UserRepository(BaseRepository):     # inherits BaseRepository
    def find_by_id(self, id: int):          # override
        return self.db.query(f"SELECT * FROM users WHERE id={id}")

    def find_by_email(self, email: str):
        return self.db.query(f"SELECT * FROM users WHERE email='{email}'")

# Multiple Inheritance
class TimestampMixin:
    def created_at(self): return "now"

class AuditMixin:
    def audit_log(self): return "logged"

class AuditedUserRepo(UserRepository, TimestampMixin, AuditMixin):
    pass

# Inspect MRO — shows lookup order
print(AuditedUserRepo.__mro__)
# (AuditedUserRepo, UserRepository, BaseRepository, TimestampMixin, AuditMixin, object)

# super() follows MRO — always prefer it over direct parent calls
class ExtendedRepo(UserRepository):
    def find_by_id(self, id: int):
        result = super().find_by_id(id)  # calls UserRepository.find_by_id
        return {"data": result, "cached": False}

1.2.3 Composition
🧩 Has-a vs Is-a (Composition over Inheritance)
Composition ("has-a") is often preferred over inheritance ("is-a"). Instead of subclassing, you inject dependencies as constructor parameters. FastAPI's dependency injection is built on this principle.
python
# ❌ Inheritance approach — tightly coupled
class UserService(SQLAlchemyEngine):  # BAD: UserService IS a DB engine?
    pass

# ✅ Composition approach — loosely coupled, testable
class UserRepository:
    def __init__(self, db: AsyncSession):
        self.db = db                    # HAS-A database session

    async def get(self, user_id: int):
        return await self.db.get(User, user_id)

class EmailService:
    def __init__(self, smtp_host: str):
        self.smtp_host = smtp_host

    async def send(self, to: str, body: str): ...

class UserService:
    def __init__(
        self,
        repo: UserRepository,          # HAS-A repo
        email: EmailService,           # HAS-A email service
    ):
        self.repo = repo
        self.email = email

    async def register(self, user_data: dict):
        user = await self.repo.create(user_data)
        await self.email.send(user.email, "Welcome!")
        return user

# FastAPI DI wires this automatically:
# async def get_user_service(db=Depends(get_db)) -> UserService:
#     return UserService(repo=UserRepository(db), email=EmailService(...))

1.2.4 Mixins
🔧 Reusable Mixin Patterns
A Mixin is a small class that provides specific, reusable behavior meant to be mixed into other classes. Mixins don't stand alone — they supplement the main class without creating deep inheritance trees.
python
# Mixin: adds created_at / updated_at to any SQLAlchemy model
from datetime import datetime

class TimestampMixin:
    created_at: datetime = Field(default_factory=datetime.utcnow)
    updated_at: datetime = Field(default_factory=datetime.utcnow)

    def touch(self):
        self.updated_at = datetime.utcnow()

class SoftDeleteMixin:
    deleted_at: datetime | None = None

    def soft_delete(self):
        self.deleted_at = datetime.utcnow()

    def is_deleted(self) -> bool:
        return self.deleted_at is not None

# Use mixins to compose rich models cleanly
class UserModel(TimestampMixin, SoftDeleteMixin, BaseModel):
    id: int
    name: str
    email: str
    # inherits: created_at, updated_at, touch(), soft_delete(), is_deleted()

user = UserModel(id=1, name="Alice", email="a@b.com")
user.touch()          # update timestamp
user.soft_delete()    # mark as deleted without DB DELETE
print(user.is_deleted())  # True

1.2.5 Abstract Classes
📜 ABC & Abstract Methods
Abstract classes define a contract — a set of methods that subclasses MUST implement. You cannot instantiate an abstract class directly. Use abc.ABC and @abstractmethod. This is the foundation for the Repository Pattern in FastAPI.
python
from abc import ABC, abstractmethod
from typing import Generic, TypeVar

T = TypeVar('T')

# Abstract base — defines the interface (contract)
class BaseRepository(ABC, Generic[T]):

    @abstractmethod
    async def get(self, id: int) -> T | None: ...

    @abstractmethod
    async def create(self, data: dict) -> T: ...

    @abstractmethod
    async def delete(self, id: int) -> bool: ...

# Concrete implementation
class SQLUserRepository(BaseRepository[User]):
    def __init__(self, session: AsyncSession):
        self.session = session

    async def get(self, id: int) -> User | None:
        return await self.session.get(User, id)

    async def create(self, data: dict) -> User:
        user = User(**data)
        self.session.add(user)
        await self.session.commit()
        return user

    async def delete(self, id: int) -> bool:
        user = await self.get(id)
        if user:
            await self.session.delete(user)
            await self.session.commit()
            return True
        return False

# Can't instantiate abstract class:
# repo = BaseRepository()  → TypeError!
# Must use concrete: repo = SQLUserRepository(session)

# Benefit: swap implementations easily in tests
class InMemoryUserRepository(BaseRepository[User]):
    def __init__(self): self._store = {}
    async def get(self, id): return self._store.get(id)
    async def create(self, data): ...
    async def delete(self, id): ...
Topic 1 · Section 1.3
Python Typing System
FastAPI is type-first — it reads your type annotations to automatically parse requests, validate data, and generate OpenAPI docs. A deep understanding of Python's typing system is non-negotiable.
1.3.1 Basic Types
🔤 int, str, float, bool — Type Annotations
Python type annotations are hints for tools (like FastAPI, mypy, and your IDE) — they don't enforce types at runtime by themselves. FastAPI DOES enforce them at request time using Pydantic.
python
# Basic type annotations
def greet(name: str, repeat: int = 1) -> str:
    return (name + " ") * repeat

# Variable annotations
user_id: int = 42
is_active: bool = True
price: float = 9.99
username: str = "alice"

# Collection types
from typing import List, Dict, Tuple, Set  # Python 3.8 style
tags: List[str] = ["fastapi", "python"]

# Python 3.9+ — use built-in generics directly
tags: list[str] = ["fastapi", "python"]
meta: dict[str, int] = {"age": 30}
coords: tuple[float, float] = (12.5, 77.6)

# FastAPI uses these to generate OpenAPI schema automatically
# @app.get("/users/{user_id}")
# async def get_user(user_id: int):   ← FastAPI validates int
#     ...                              ← 422 if non-integer given

1.3.2 Advanced Types
Optional & Union
Optional[X] is shorthand for Union[X, None] — means the value can be X or None. Union[X, Y] means either X or Y. Python 3.10+ uses the | operator as a shorthand.
python
from typing import Optional, Union

# Optional — value can be str or None
def find_user(email: Optional[str] = None) -> Optional[dict]:
    if email is None:
        return None
    return {"email": email}

# Union — accepts multiple types
def process(value: Union[int, str]) -> str:
    return str(value)

# Python 3.10+ shorthand (preferred now)
def find_user(email: str | None = None) -> dict | None: ...
def process(value: int | str) -> str: ...

# FastAPI query params example:
# @app.get("/users")
# async def list_users(role: str | None = None):  ← optional filter
#     ...
🎯 Literal, Any & Generic
Literal constrains a value to specific literal values. Any disables type checking (use sparingly). Generic lets you build reusable typed containers like a generic Response wrapper.
python
from typing import Literal, Any, Generic, TypeVar

# Literal — only these exact values allowed
Status = Literal["active", "inactive", "pending"]

def set_status(status: Status) -> None: ...
set_status("active")    # ✅ OK
# set_status("deleted") → mypy error

# Any — opt out of type checking (avoid in production code)
def legacy_func(data: Any) -> Any:
    return data

# Generic — build reusable typed wrappers
T = TypeVar('T')

class APIResponse(Generic[T]):
    def __init__(self, data: T, message: str = "success"):
        self.data = data
        self.message = message
        self.success = True

# Typed responses for different resources
user_resp: APIResponse[User] = APIResponse(data=user)
list_resp: APIResponse[list[User]] = APIResponse(data=users)

# Pydantic v2 Generic Model (used in FastAPI responses)
from pydantic import BaseModel

class PaginatedResponse(BaseModel, Generic[T]):
    items: list[T]
    total: int
    page: int
    size: int

# @app.get("/users", response_model=PaginatedResponse[UserOut])

1.3.3 Structural Typing
🔗 Protocol — Duck Typing, Typed
Protocol enables structural (duck) typing — a class satisfies the protocol if it has the right methods, without explicitly inheriting from it. Perfect for defining interfaces that third-party classes should satisfy.
python
from typing import Protocol, runtime_checkable

@runtime_checkable
class Cacheable(Protocol):
    def cache_key(self) -> str: ...
    def to_dict(self) -> dict: ...

# Any class with these methods satisfies Cacheable
class UserDTO:
    def __init__(self, id: int, name: str):
        self.id = id; self.name = name

    def cache_key(self) -> str:
        return f"user:{self.id}"

    def to_dict(self) -> dict:
        return {"id": self.id, "name": self.name}

def cache_object(obj: Cacheable, redis): # accepts anything with cache_key+to_dict
    redis.set(obj.cache_key(), obj.to_dict())

user = UserDTO(1, "Alice")
print(isinstance(user, Cacheable))  # True (runtime check)
cache_object(user, redis_client)    # ✅ UserDTO never imports Cacheable
🗂️ TypedDict
TypedDict lets you define the expected shape of a dictionary with typed keys. Useful when working with legacy code or external APIs that return plain dicts rather than Pydantic models.
In FastAPI: Prefer Pydantic BaseModel over TypedDict for request/response bodies — you get validation, serialization, and docs for free. Use TypedDict for internal data structures where you want typing without Pydantic overhead.
python
from typing import TypedDict, Required, NotRequired

# Define a typed dict shape
class UserDict(TypedDict):
    id: int
    name: str
    email: str

# With optional fields (Python 3.11+)
class UserUpdateDict(TypedDict, total=False):  # all keys optional
    name: str
    email: str

# Or mix required and optional (Python 3.11+)
class UserCreateDict(TypedDict):
    name: Required[str]
    email: Required[str]
    role: NotRequired[str]        # optional

def process_user(user: UserDict) -> str:
    return f"{user['name']} <{user['email']}>"

data: UserDict = {"id": 1, "name": "Alice", "email": "a@b.com"}
print(process_user(data))  # Alice <a@b.com>
Topic 1: Python Foundations  ·  3 sections · 14 concepts · All subtopics covered
✅ Approved — Ready for Topic 2