Project structure and boundaries
Build installable Python packages with a src layout and explicit separation of concerns.
Project structure should make valid imports, ownership, and runtime entry points obvious. Folders are not architecture by themselves; boundaries matter when they constrain dependencies and keep changes local.
A maintainable src layout
inventory-service/
├── pyproject.toml
├── README.md
├── src/
│ └── inventory/
│ ├── __init__.py
│ ├── domain/
│ │ ├── models.py
│ │ └── pricing.py
│ ├── application/
│ │ └── reserve_stock.py
│ ├── adapters/
│ │ ├── database.py
│ │ └── http.py
│ └── cli.py
└── tests/
├── unit/
└── integration/The src layout prevents tests from accidentally importing the repository directory instead
of the installed package. Install the project in editable mode during development:
python -m pip install -e .
python -m pytestAlways run modules through the active interpreter with python -m ... when interpreter
selection matters.
Package metadata
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "inventory-service"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
[project.scripts]
inventory = "inventory.cli:main"Pin application environments with a lock or constraints strategy, but keep reusable library requirements broad enough to compose with consumers. Record the supported Python versions explicitly.
Separate concerns by dependency direction
A useful flow is:
interfaces/adapters -> application use cases -> domain rulesThe domain should not import the CLI framework, web framework, database driver, or environment loader. Application code coordinates work. Adapters translate external representations.
from dataclasses import dataclass
from typing import Protocol
@dataclass(frozen=True)
class Reservation:
sku: str
quantity: int
class StockRepository(Protocol):
def available(self, sku: str) -> int: ...
def reserve(self, reservation: Reservation) -> None: ...
def reserve_stock(repository: StockRepository, request: Reservation) -> None:
if request.quantity <= 0:
raise ValueError("Quantity must be positive")
if repository.available(request.sku) < request.quantity:
raise ValueError(f"Insufficient stock for {request.sku}")
repository.reserve(request)The use case depends on behavior, not a concrete database. Tests can supply a small in-memory repository without mocking implementation details.
Imports express architecture
Prefer absolute package imports:
from inventory.domain.models import ReservationRelative imports can be reasonable inside a tightly cohesive subpackage, but long chains such
as from ....domain signal excessive nesting. Avoid modifying sys.path; install the package
correctly instead.
Keep __init__.py lightweight. Re-export only a deliberate public API. Import-time database
connections, file reads, and environment validation make tests and tooling fragile.
Tests mirror behavior, not every file
- Unit tests exercise domain rules with no network, database, or clock dependency.
- Integration tests verify boundaries such as SQL mappings or HTTP clients.
- End-to-end tests verify a few critical flows through the real entry point.
Do not duplicate the entire source tree mechanically. Group tests by behavior when that makes the contract easier to find.
Pitfalls
- a
utils.pythat becomes the owner of unrelated code; - circular imports caused by layers depending in both directions;
- framework objects passed deep into domain logic;
- global clients created during import;
- tests that pass only from the repository root;
- one module per tiny class, producing navigation overhead without isolation;
- a top-level package name that collides with the standard library.
Structure checklist
- The project installs and imports outside its repository root.
- Runtime entry points are declared in
pyproject.toml. - Domain rules do not depend on delivery or storage frameworks.
- External I/O is isolated behind narrow adapters.
- Import direction is acyclic and explainable.
- Unit and integration tests have distinct purposes.
- Package initialization has no surprising side effects.
Practice
Take a script that reads a file, applies business rules, and prints output. Split it into: an adapter that reads input, a pure transformation, and a presentation adapter. Write a unit test for the transformation and an integration test for the file boundary.
LEARNING RECORD
Finish this lesson
Mark it complete when you can explain the main decision without looking.
KNOWLEDGE CHECK