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 fromservicesor adapters — no HTTP, noos.environservicesaccept collaborators as parameters (constructor injection), so tests pass fakescli.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?
2. Where should os.environ be read in a well-layered app?
3. Why order CI gates format → lint → type → test?
Flashcards
Question
src layout
Click to reveal answer
Answer
Package under src/ so tests exercise the installed distribution, not cwd imports.
Question
PEP 621 pyproject
Click to reveal answer
Answer
[project] table holds name, version, deps, scripts — single source of package metadata.
Question
Ports and adapters
Click to reveal answer
Answer
Domain at the center; IO adapters at the edges, wired by constructors for testability.
Question
CI gate order
Click to reveal answer
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
Before You Start
- No prerequisites listed.