Python · Advanced

Tests, type hints and a project layout that survives review

3 min read

Why it matters

At the Architect floor the question changes from "does it work?" to "can the team trust and change it?". Data engineering, ML engineering and backend job descriptions ask for "unit testing", "code quality" and "CI/CD". Reviewers look for tests and types before they read your logic.

The concept

  • Project layout: code in a package under src/, tests in tests/, configuration in pyproject.toml.

  • pytest: plain functions named test_* with assert. Fixtures supply shared setup.

  • Type hints: def mean(values: list[float]) -> float. A checker such as mypy or pyright finds mistakes before runtime; editors use hints for autocomplete.

  • Linting and formatting: Ruff checks style and common bugs and formats code in one fast tool.

  • CI: run all of it on every push, so broken code never reaches the main branch.

Worked example: a tested helper package

layout
retail-metrics/
  pyproject.toml
  src/retail_metrics/__init__.py
  src/retail_metrics/metrics.py
  tests/test_metrics.py
src/retail_metrics/metrics.py
from dataclasses import dataclass
from datetime import date


@dataclass(frozen=True)
class Line:
    invoice: str
    quantity: int
    price: float
    day: date


def is_sale(line: Line) -> bool:
    """A real sale: not a cancellation (invoice starting with C) and a positive quantity."""
    return not line.invoice.startswith("C") and line.quantity > 0


def revenue(lines: list[Line]) -> float:
    return round(sum(line.quantity * line.price for line in lines if is_sale(line)), 2)
tests/test_metrics.py
from datetime import date

import pytest

from retail_metrics.metrics import Line, is_sale, revenue

D = date(2011, 12, 9)


@pytest.mark.parametrize(
    "line, expected",
    [
        (Line("581483", 80995, 2.08, D), True),
        (Line("C581484", -80995, 2.08, D), False),     # the matching cancellation
        (Line("581485", 0, 1.0, D), False),
    ],
)
def test_is_sale(line: Line, expected: bool) -> None:
    assert is_sale(line) is expected


def test_revenue_ignores_cancellations() -> None:
    lines = [Line("1", 2, 1.5, D), Line("C2", -2, 1.5, D)]
    assert revenue(lines) == 3.0


def test_revenue_of_nothing_is_zero() -> None:
    assert revenue([]) == 0
pyproject.toml
[project]
name = "retail-metrics"
version = "0.1.0"
requires-python = ">=3.11"

[project.optional-dependencies]
dev = ["pytest", "mypy", "ruff"]

[tool.pytest.ini_options]
pythonpath = ["src"]

[tool.ruff]
line-length = 100
check.sh
pip install -e ".[dev]"
ruff check . && ruff format --check .
mypy src
pytest -q

The test cases are real: invoice 581483 in the UCI Online Retail data is the 80,995-unit order, and C581484 is its cancellation. Tests built from real incidents are the most valuable ones you can write.

Common mistakes

  • Testing only the happy path. Empty input, cancellations and zero quantities are where the bugs are.

  • Tests that depend on the network or today's date. Pass dates in; fake the network.

  • Type hints without a checker. Hints nobody checks drift out of date. Run mypy or pyright in CI.

  • One giant test. One behaviour per test makes failures easy to read.

Interview angle

"How do you make sure your code is correct?" Answer with tests for edge cases, types checked in CI and a linter, and an example of a bug a test caught. For data roles add data tests (row counts, uniqueness) as a second layer.

Practice task

  1. Add revenue_by_day(lines) -> dict[date, float] with tests, including a day with only cancellations.

  2. Add a GitHub Actions workflow that runs ruff, mypy and pytest on every push.

  3. Make one test fail on purpose and read the pytest output.

Further reading