Skip to content
advanced Phase 20 · Python Capstone Project

Project Architecture

Design a Python application with proper packaging, testing, and CI/CD.

1h
0 problems
Topic Progress 0%

Layout and Packaging

Layout and Packaging

proj/
├── pyproject.toml        # PEP 621 metadata, deps, tool config — one file
├── src/
│   └── proj/
│       ├── __init__.py
│       ├── cli.py        # console entry point
│       └── services/
└── tests/
    ├── unit/
    └── integration/
[project]
name = "proj"
version = "0.1.0"
dependencies = ["httpx>=0.27"]

[project.scripts]
proj = "proj.cli:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

The src layout matters: with src/, tests cannot accidentally import your package from the working directory instead of the installed distribution — the classic "works locally, fails in CI" trap. [project.scripts] installs a real console command; pin dev tools in an optional dependency group ([project.optional-dependencies] dev) so environments stay reproducible.

Layering and Boundaries

Layering and Boundaries

Keep domain rules pure and push I/O to the edges:

cli / api adapters  →  services (use cases)  →  domain (pure logic)
        ↑                       ↑
   config (env)          repositories (db, http)

Rules that keep the diagram true:

  • domain/ imports nothing from services or adapters — no HTTP, no os.environ
  • services accept collaborators as parameters (constructor injection), so tests pass fakes
  • cli.main() is thin: parse args, load config, call one service, map result to exit code
  • Public surface is explicit — re-export in __init__.py

Configuration lives in one module that reads os.environ once, validates it, and returns an immutable object. Domain code receives that object; it never touches os.environ directly. The payoff is testability: make_service(client=fake_http) needs no monkeypatching.

Testing Strategy and CI/CD

Testing Strategy and CI/CD

Match test scope to blast radius:

Tier Scope Speed Examples
unit pure functions, no I/O ms pricing rules, parsers
integration one real dependency s DB via tmp_path, filesystem
end-to-end full stack min CLI smoke run, API happy path

CI runs gates in cheap-to-expensive order so failures surface fast:

- run: ruff check . && black --check .
- run: mypy src
- run: pytest -n auto --cov=src --cov-fail-under=80

Cache the virtualenv between runs; matrix across the Python versions you support (3.11, 3.12, 3.13). CD publishes only from tags: python -m build, twine check dist/*, then upload with provenance/trusted publishing. Keep main green — broken merges cost far more than slow pipelines.

Quiz

1. Why use a src/ layout instead of a flat package at the repo root?

Question 1 options

2. Where should os.environ be read in a well-layered app?

Question 2 options

3. Why order CI gates format → lint → type → test?

Question 3 options

Flashcards

Question

src layout

Answer

Package under src/ so tests exercise the installed distribution, not cwd imports.

Question

PEP 621 pyproject

Answer

[project] table holds name, version, deps, scripts — single source of package metadata.

Question

Ports and adapters

Answer

Domain at the center; IO adapters at the edges, wired by constructors for testability.

Question

CI gate order

Answer

format check → lint → mypy → tests+coverage; cheapest signal first, cache deps.

Revision Notes

Key Takeaways

  • 1. src layout + pyproject eliminates packaging and environment drift early
  • 2. Pure domain core with injected adapters keeps tests fast and honest
  • 3. CI enforces the same gates developers run locally, cheapest first

Interview Tips

  • • Sketch the repo tree and layer diagram from memory — structure questions reward concreteness
  • • Explain fail-fast config and how injection replaces monkeypatching in tests

Cheat Sheet

src/ layout + [project] scripts; layer cli→services→domain, config read once; CI: ruff/black --check → mypy → pytest --cov-fail-under; tag-driven release.

Continue Learning