Python Atlas
Software engineering

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 pytest

Always 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 rules

The 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 Reservation

Relative 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.py that 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

Project structure and boundaries

1What important testing problem does a src layout help expose?
2Which dependency direction best preserves explicit architectural boundaries?