Learn / Python and FastAPI Backend Engineering
Testing, Packaging, and Operations
Package a FastAPI service so reviewers can prove route contracts, dependency overrides, startup commands, and liveness/readiness behavior before traffic reaches it.
Course: Python and FastAPI Backend Engineering. Level: Intermediate. Topic: Backend craft.
Stage: intermediate - Intermediate verification - FastAPI testing, packaging, and runtime checks. Turn local API code into a repeatable service package with tests, health checks, and startup evidence.
Outcomes
- Test success, validation, auth, and repository-failure paths through the ASGI boundary.
- Package the service with repeatable startup and environment proof instead of laptop-only commands.
- Split liveness from readiness so deploys know when the process is alive and when dependencies can receive traffic.
- Explain the difference between Python, pip, venv, and a project.
- Create and activate a virtual environment.
- Run code as a script and as a module.
- Keep configuration, dependencies, and source files separated.
- Write focused unit tests for pure Python behavior.
- Use FastAPI TestClient for API contract tests.
- Override dependencies to avoid real external services.
- Keep exercises safe with text checks, canned outputs, and choices.
- Know what belongs in pyproject.toml.
- Use formatters and linters to automate style decisions.
- Run type checks as a quality gate.
- Design a local check command that CI can repeat.
- Identify common packages for HTTP, data, settings, databases, and tasks.
- Choose synchronous versus asynchronous libraries intentionally.
- Evaluate package maintenance and security posture.
- Avoid adding dependencies for trivial standard-library features.
Concepts
- ASGI contract test
- dependency override fixture
- startup proof
- liveness/readiness split
- Python Orientation and Environment
- Why environments matter
- Scripts versus modules
- Python + FastAPI foundations
- Guided practice
- Testing
- Test behavior, not implementation trivia
- FastAPI API tests
- Tooling and Quality
- One project configuration
- Repeatable checks
- Popular Packages
- Packages by job
- Dependency selection
Concept flow
Show how route contract tests, package startup, and health endpoints become deploy evidence for a production service.
- pytest
- ASGI client
- FastAPI app
- Override fixture
- Fake repository
- Health endpoints
- Deploy gate
Session flow
- Test through the real API boundary (concept, 14 min) — Use an ASGI client to prove request parsing, validation, dependency resolution, response models, and expected failure behavior.
- Handler-only tests miss the boundary most clients experience.
- A useful route contract test includes status, body shape, and one important side effect or fake interaction.
- Make dependency overrides boring and isolated (walkthrough, 18 min) — Install fake repositories, auth, and settings through fixtures that clean up app-level override state.
- Override fixtures should be scoped, named, and cleared.
- Repository failures deserve client-safe responses and operator-facing logs.
- Package startup as review evidence (exercise, 12 min) — Document the clean install command, run command, required env names, and captured startup/readiness output.
- A deployable service has a repeatable command, not a memory of how one laptop ran it.
- Startup proof should be safe to paste into a pull request without secrets.
- Split liveness from readiness (exercise, 10 min) — Keep liveness cheap and make readiness prove required dependencies before traffic admission.
- Liveness restarts broken processes; readiness protects users from instances that are not ready.
- Readiness should fail closed when a required dependency is unavailable.
- Environment, Toolchain, and First Program: Why environments matter (concept, 45 min) — Python projects share an interpreter but should not share dependencies accidentally. A virtual environment gives each project its own import location, which makes installs repeatable and reduces conflicts between applications.
- Use one environment per project.
- Commit dependency declarations, not the environment folder.
- Prefer python -m pip so pip runs under the interpreter you intend.
- If python points to the wrong version, use python3 or a version manager.
- On Windows, activation uses .venv\Scripts\activate.
- Retained source example: Create a local environment
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install fastapi uvicorn
The .venv directory belongs to your workstation. The dependency names belong in project metadata.
- Environment, Toolchain, and First Program: Scripts versus modules (walkthrough, 45 min) — A script is run by filename. A module is run by import path. Running packages as modules keeps relative imports and package discovery predictable.
- Retained source example: A tiny module entry point
def main() -> None:
print('hello from the project')
if __name__ == '__main__':
main()
Expected output: hello from the project
The guard lets the file be imported without immediately running command-line behavior.
- Environment, Toolchain, and First Program: practices and mistakes (review, 5 min) — Retained guidance from the legacy PTesting lesson.
- Practice: Create the environment inside or next to the project and exclude it from version control.
- Practice: Run package tools with python -m when possible.
- Practice: Document the minimum Python version and startup command.
- Avoid: Installing dependencies globally and then wondering why deployments differ.
- Avoid: Running files from random working directories until imports accidentally work.
- Avoid: Committing .venv or generated cache directories.
- Environment, Toolchain, and First Program: references (review, 2 min) — Original references retained from the legacy library.
- Python tutorial: https://docs.python.org/3/tutorial/
- Python standard library: https://docs.python.org/3/library/
- Pytest, TestClient, and Dependency Overrides: Test behavior, not implementation trivia (concept, 75 min) — Good tests describe visible behavior and important edge cases. Pure functions are cheapest to test; route tests should cover HTTP details such as status codes, validation errors, and response shapes.
- Retained source example: Simple unit test
def normalize_tag(value: str) -> str:
return value.strip().lower().replace(' ', '-')
def test_normalize_tag() -> None:
assert normalize_tag(' Fast API ') == 'fast-api'
- Pytest, TestClient, and Dependency Overrides: FastAPI API tests (walkthrough, 75 min) — TestClient runs the ASGI app in-process for tests. It is useful for checking routing, validation, dependency overrides, and JSON responses without starting a live server.
- Retained source example: Endpoint test
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_health() -> None:
response = client.get('/hhealth')
assert response.status_code == 200
assert response.json()['status'] == 'healthy'
- Pytest, TestClient, and Dependency Overrides: practices and mistakes (review, 5 min) — Retained guidance from the legacy PTesting lesson.
- Practice: Favor deterministic tests with explicit fixtures.
- Practice: Test validation errors as well as happy paths.
- Practice: Keep route tests fewer and meaningful; push business rules into unit-tested services.
- Avoid: Testing private implementation details that change during refactoring.
- Avoid: Letting tests depend on live networks or shared databases by default.
- Avoid: Forgetting to clear dependency overrides after a test.
- Pytest, TestClient, and Dependency Overrides: references (review, 2 min) — Original references retained from the legacy library.
- pytest documentation: https://docs.pytest.org/
- FastAPI testing: https://fastapi.tiangolo.com/tutorial/testing/
- pyproject.toml, Ruff, Typing, and CI: One project configuration (concept, 60 min) — Modern Python tooling usually reads pyproject.toml. Keeping formatter, lint, test, and type settings together makes the project easier to clone and automate.
- Retained source example: Minimal quality settings
[tool.ruff]
line-length = 100
[tool.ruff.lint]
select = ['E', 'F', 'I', 'B']
[tool.pytest.ini_options]
testpaths = ['tests']
- pyproject.toml, Ruff, Typing, and CI: Repeatable checks (walkthrough, 60 min) — A project should have one documented way to run checks locally. CI should run the same commands so developers see failures before pushing.
- Retained source example: Local quality gate
python -m ruff format --check .
python -m ruff check .
python -m pytest
- pyproject.toml, Ruff, Typing, and CI: practices and mistakes (review, 5 min) — Retained guidance from the legacy PTesting lesson.
- Practice: Automate formatting so reviews focus on behavior.
- Practice: Fail CI on lint and test errors.
- Practice: Pin or lock production dependencies for deployable applications.
- Avoid: Using different commands locally and in CI.
- Avoid: Ignoring dependency updates until a security fix becomes urgent.
- Avoid: Adding lint rules without fixing or documenting the migration path.
- pyproject.toml, Ruff, Typing, and CI: references (review, 2 min) — Original references retained from the legacy library.
- Python pyproject.toml: https://packaging.python.org/en/latest/guides/writing-pyproject-toml/
- Ruff documentation: https://docs.astral.sh/ruff/
- mypy documentation: https://mypy.readthedocs.io/
- A Practical Backend Package Map: Packages by job (concept, 60 min) — A mature Python backend often combines FastAPI and Pydantic with tools such as httpx for HTTP clients, SQLAlchemy or sqlite3 for persistence, pytest for tests, Ruff for formatting and linting, and python-dotenv or Pydantic settings for local configuration.
- Retained source example: HTTP client with timeout
import httpx
def fetch_status(url: str) -> int:
with httpx.Client(timeout=5.0) as client:
response = client.get(url)
return response.status_code
- A Practical Backend Package Map: Dependency selection (walkthrough, 60 min) — Before adding a package, check whether the standard library is enough, whether the package is actively maintained, whether it supports your Python version, and whether it matches your sync or async architecture.
- Retained source example: Selection checklist
Need: outbound HTTP
Options: urllib.request, requests, httpx, aiohttp
Decision: httpx when one project may need both sync and async clients
- A Practical Backend Package Map: practices and mistakes (review, 5 min) — Retained guidance from the legacy PTesting lesson.
- Practice: Use timeouts for outbound I/O libraries.
- Practice: Keep dependency count intentional and reviewed.
- Practice: Prefer packages with clear docs, recent releases, and broad ecosystem use.
- Avoid: Mixing sync and async clients carelessly.
- Avoid: Adding packages for one-line standard-library tasks.
- Avoid: Ignoring transitive dependency risk.
- A Practical Backend Package Map: references (review, 2 min) — Original references retained from the legacy library.
- httpx: https://www.python-httpx.org/
- SQLAlchemy: https://docs.sqlalchemy.org/
- PyPI security practices: https://packaging.python.org/en/latest/specifications/core-metadata/
Code example
Python in tests/test_items.py.
from collections.abc import AsyncIterator
from dataclasses import dataclass
import pytest
from fastapi import APIRouter, Depends, FastAPI, HTTPException, status
from httpx import ASGITransport, AsyncClient
from pydantic import BaseModel, Field
pytestmark = pytest.mark.anyio
class ItemCreate(BaseModel):
name: str = Field(min_length=2, max_length=80)
quantity: int = Field(ge=0, le=500)
class ItemRead(BaseModel):
id: str
name: str
quantity: int
class ItemRepository:
async def create(self, payload: ItemCreate) -> ItemRead:
raise NotImplementedError
async def ready(self) -> bool:
raise NotImplementedError
@dataclass
class FakeItemRepository(ItemRepository):
fail_writes: bool = False
ready_state: bool = True
created_count: int = 0
async def create(self, payload: ItemCreate) -> ItemRead:
if self.fail_writes:
raise RuntimeError("database unavailable")
self.created_count += 1
return ItemRead(id="item_123", name=payload.name, quantity=payload.quantity)
async def ready(self) -> bool:
return self.ready_state
async def get_item_repository() -> ItemRepository:
return ItemRepository()
router = APIRouter(prefix="/items", tags=["items"])
@router.post("", response_model=ItemRead, status_code=status.HTTP_201_CREATED)
async def create_item(
payload: ItemCreate,
repository: ItemRepository = Depends(get_item_repository),
) -> ItemRead:
try:
return await repository.create(payload)
except RuntimeError as exc:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Items are temporarily unavailable.",
) from exc
def create_app() -> FastAPI:
app = FastAPI(title="Items API", version="2026.06.30")
app.include_router(router)
@app.get("/health/live", include_in_schema=False)
async def live() -> dict[str, str]:
return {"status": "alive"}
@app.get("/health/ready", include_in_schema=False)
async def ready(repository: ItemRepository = Depends(get_item_repository)) -> dict[str, str]:
if not await repository.ready():
raise HTTPException(status_code=503, detail="Database is not ready.")
return {"status": "ready"}
return app
@pytest.fixture
async def client_with_fake_repo() -> AsyncIterator[tuple[AsyncClient, FakeItemRepository]]:
app = create_app()
fake_repo = FakeItemRepository()
app.dependency_overrides[get_item_repository] = lambda: fake_repo
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://testserver",
) as client:
yield client, fake_repo
app.dependency_overrides.clear()
async def test_create_item_contract(client_with_fake_repo):
client, fake_repo = client_with_fake_repo
response = await client.post("/items", json={"name": "Notebook", "quantity": 3})
assert response.status_code == 201
assert response.json() == {"id": "item_123", "name": "Notebook", "quantity": 3}
assert fake_repo.created_count == 1
async def test_validation_contract(client_with_fake_repo):
client, _fake_repo = client_with_fake_repo
response = await client.post("/items", json={"name": "N", "quantity": -1})
assert response.status_code == 422
assert response.json()["detail"][0]["loc"][0] == "body"
async def test_repository_failure_is_client_safe(client_with_fake_repo):
client, fake_repo = client_with_fake_repo
fake_repo.fail_writes = True
response = await client.post("/items", json={"name": "Notebook", "quantity": 3})
assert response.status_code == 503
assert response.json()["detail"] == "Items are temporarily unavailable."
async def test_liveness_and_readiness_are_separate(client_with_fake_repo):
client, fake_repo = client_with_fake_repo
assert (await client.get("/health/live")).status_code == 200
fake_repo.ready_state = False
response = await client.get("/health/ready")
assert response.status_code == 503
assert response.json()["detail"] == "Database is not ready."
Walkthrough examples
- Deployable FastAPI service proof pack — A team wants to merge a new item API only if another engineer can test it, start it, and prove it should receive traffic.
- File: app/main.py
- File: app/items/routes.py
- File: app/items/dependencies.py
- File: tests/test_items_contract.py
- File: Dockerfile
- File: docs/ops/startup-proof.md
- Create create_app() so tests and production use the same router and provider graph.
- Write ASGI route tests for success, validation failure, auth failure, repository failure, liveness, and readiness.
- Add dependency override fixtures for auth, repository, and settings; clear overrides in teardown.
- Document uvicorn or container startup with required environment names and a captured readiness response.
- Add a review checklist that names the command evidence required before deploy.
- Create a local environment — The .venv directory belongs to your workstation. The dependency names belong in project metadata.
- Retained source code:
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install fastapi uvicorn
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- A tiny module entry point — The guard lets the file be imported without immediately running command-line behavior.
- Retained source code:
def main() -> None:
print('hello from the project')
if __name__ == '__main__':
main()
- Expected output: hello from the project
- Compare the example with the canonical PTLearn implementation.
- Small service layout — Application code, tests, dependency metadata, and docs have separate homes.
- Retained source code:
my_api/
app/
__init__.py
main.py
tests/
test_health.py
pyproject.toml
README.md
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Simple unit test — Additional example retained from the legacy lesson.
- Retained source code:
def normalize_tag(value: str) -> str:
return value.strip().lower().replace(' ', '-')
def test_normalize_tag() -> None:
assert normalize_tag(' Fast API ') == 'fast-api'
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Endpoint test — Additional example retained from the legacy lesson.
- Retained source code:
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_health() -> None:
response = client.get('/hhealth')
assert response.status_code == 200
assert response.json()['status'] == 'healthy'
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Dependency override shape — Additional example retained from the legacy lesson.
- Retained source code:
app.dependency_overrides[get_current_user] = lambda: {'id': 'test-user'}
try:
response = client.get('/me')
finally:
app.dependency_overrides.clear()
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Minimal quality settings — Additional example retained from the legacy lesson.
- Retained source code:
[tool.ruff]
line-length = 100
[tool.ruff.lint]
select = ['E', 'F', 'I', 'B']
[tool.pytest.ini_options]
testpaths = ['tests']
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Local quality gate — Additional example retained from the legacy lesson.
- Retained source code:
python -m ruff format --check .
python -m ruff check .
python -m pytest
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Common quality layers — Additional example retained from the legacy lesson.
- Retained source code:
format -> lint -> type check -> tests -> package or container build
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- HTTP client with timeout — Additional example retained from the legacy lesson.
- Retained source code:
import httpx
def fetch_status(url: str) -> int:
with httpx.Client(timeout=5.0) as client:
response = client.get(url)
return response.status_code
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Selection checklist — Additional example retained from the legacy lesson.
- Retained source code:
Need: outbound HTTP
Options: urllib.request, requests, httpx, aiohttp
Decision: httpx when one project may need both sync and async clients
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Useful package categories — Additional example retained from the legacy lesson.
- Retained source code:
API: fastapi, starlette, pydantic
HTTP clients: httpx, requests, aiohttp
Database: sqlite3, SQLAlchemy, Alembic
Testing: pytest, hypothesis, respx
Quality: ruff, mypy, bandit
Operations: structlog, prometheus-client
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
Practice
- Write route tests for success, validation failure, auth failure, and repository failure through ASGITransport or TestClient.
- Build a fixture that installs dependency overrides and clears them after the test scope.
- Run the package startup command from a clean shell and save the exact command plus the first successful log line.
- Add /health/live and /health/ready with different cost and traffic meanings.
- Confirm your interpreter: Write the command that prints the Python version for the active environment without relying on a standalone pip executable.
- Expected output: python -m pip --version
- Hint: Use python -m to run a module.
- Reference solution: python -m pip --version
- Accepted answers: python -m pip --version | python3 -m pip --version
- Write a status assertion: Provide the pytest assertion that checks a response has HTTP 200.
- Starter code: response = client.get('/hhealth')
____
- Hint: Access response.status_code.
- Reference solution: assert response.status_code == 200
- Accepted answers: assert response.status_code == 200
- Pick a repeatable check: Name one command that should be safe to run locally and in CI for tests.
- Hint: Use Python module execution for the tool.
- Reference solution: python -m pytest
- Accepted answers: python -m pytest | pytest
- Add a timeout: What keyword argument should an HTTP client call include to avoid waiting forever?
- Starter code: client.get(url, ____=5.0)
- Hint: The word is the same as the failure mode.
- Reference solution: timeout
- Accepted answers: timeout
Checklist
- Route tests hit the app boundary instead of calling handlers directly.
- Dependency overrides are installed by fixtures and cleared after each test.
- Validation and repository failures return stable client-safe payloads.
- Startup proof names required environment variables and the run command.
- Liveness is cheap, readiness checks required dependencies, and deploy gates use readiness.
- Review notes include pytest output, startup output, and readiness output.
Quiz prompts
- Why test a FastAPI route through an ASGI client instead of calling the route function directly? — Route contract tests should cover the actual web boundary clients depend on, not only the Python function body.
- What should a dependency override fixture always do after the test? — FastAPI overrides are app-level state. Cleanup is part of the test contract.
- Which signal belongs in readiness rather than liveness? — Liveness asks whether the process is alive. Readiness asks whether the instance should receive traffic now.
- A clean startup proof should include which evidence? — Startup proof should be reproducible by another engineer without inheriting your local shell state.
- What is the best response when the repository dependency fails during a create route? — Clients need stable status semantics; operators need internal logs. Do not leak implementation details in the public payload.
- Why should each Python project use its own virtual environment? — A virtual environment keeps project dependencies separate from system Python and from other projects.
- Why override dependencies in FastAPI tests? — Overrides keep tests fast, local, and predictable.
- Why should CI run the same checks developers run locally? — Shared commands keep local and remote feedback aligned.
- What should you check before adding a dependency? — Dependencies create long-term operational and security obligations.
Flashcards
- What does an ASGI route test prove? It proves the HTTP boundary: routing, validation, dependencies, serialization, and error mapping.
- Why clear FastAPI dependency overrides? Overrides are app-level state. Clearing them prevents fake auth, repositories, or settings from leaking into later tests.
- How is readiness different from liveness? Liveness says the process can answer. Readiness says the instance has required dependencies and can safely receive traffic.
- What belongs in startup proof? Install/run command, required env names, version or startup log, and readiness output without secrets.
- Why map repository outages to a stable 503? Clients get predictable retry semantics while internal exception details stay in logs and traces.
- What is the review smell in calling route functions directly for every test? It bypasses the web boundary, so validation, dependencies, response models, and exception handlers can drift untested.
Labs
- Ship a tested and startable FastAPI package — Turn a working local route into a production proof pack: boundary tests, isolated overrides, clean startup command, and health/readiness evidence.
- Expose create_app() and use it in both tests and the production run command.
- Write ASGI tests for create success, validation failure, auth failure, repository failure, /health/live, and /health/ready.
- Create fake auth and repository override fixtures that clear app.dependency_overrides after each test.
- Add a Dockerfile or runbook command that starts the app from a clean environment.
- Capture pytest output, startup output, and readiness output in docs/ops/startup-proof.md.
- The test suite proves route contracts and expected failure responses through the app boundary.
- A teammate can run the documented startup command without relying on private shell aliases.
- Readiness fails when the fake dependency is unavailable and passes when it recovers.
- The review note lists required env names but no secret values.
Challenge
- Review-ready FastAPI operations proof pack (Core) — Create the evidence a reviewer needs before approving deploy: route contract tests, isolated overrides, reproducible startup command, liveness/readiness endpoints, and a short traffic-admission note.
- Tests cover success, validation failure, auth failure, repository failure, liveness, and readiness.
- Dependency override fixtures are scoped and cleaned up.
- Startup proof includes command, env names, version or log output, and readiness response.
- Liveness and readiness have different meanings and the deploy note names which one receives traffic checks.
Canonical lesson URL