TEST-DRIVEN DEVELOPMENT IS NON-NEGOTIABLE. Every line of production code is written in direct response to a failing test—no exceptions. TDD is the keystone that supports every other principle in this document.
I practice Test-Driven Development with a strong emphasis on behavior-driven testing and functional programming principles. Work proceeds in very small, incremental changes that keep the system green at all times.
Key Principles
Principle | Python-specific Notes |
---|---|
Write tests first | Use pytest or pytest-bdd to express scenarios before implementation. |
Test behavior, not implementation | Treat modules as black boxes; assert on externally observable outcomes. |
No “dynamic” escape hatches | Avoid typing.Any , unchecked casts, or # type: ignore pragmas. |
Immutable data only | Prefer @dataclass(frozen=True) , NamedTuple , or typing.TypedDict + frozen wrappers. |
Small, pure functions | Minimize side-effects; lean on composition over shared state. |
Static type checking on | Run mypy --strict (or pyright --strict ) in CI; no warnings allowed. |
Re-use real domain types in tests | Import the same dataclasses / pydantic models your app uses—never re-declare shapes in test code. |
Preferred Tools
- Language: Python 3.12+ (PEP 695 type-parameter syntax)
- Testing:
pytest
±pytest-bdd
orbehave
- Property Testing:
hypothesis
- Mocking / Stubbing:
pytest-mock
,responses
, MSW-py (Mock Service Worker analogue for HTTPX / aiohttp) - Lint / Static Analysis:
ruff
,mypy --strict
,pyright
- State Management: Immutable data structures (
attrs
,frozen dataclasses
,immutables.Map
) and pure functions
- Scrap the term “unit test.” A test’s granularity is less important than its focus on externally visible behavior.
- Interact strictly through the module’s public API—internal helpers are invisible.
- No 1:1 test-to-file mapping. Organize tests around user stories, features, or public boundaries.
- Tests that crack open internals are a maintenance burden—avoid them.
- Coverage target: 100 % line + branch coverage, but every assertion must track to business behavior, not code paths.
- A test suite is executable documentation of business expectations.
Purpose | Library |
---|---|
Test runner / assertions | pytest |
BDD syntax (optional) | pytest-bdd or behave |
Property-based checks | hypothesis |
HTTP / API mocking | responses , pytest-httpx , or MSW-py |
Static typing in tests | Same strict mypy/pyright settings as production code |
project_root/
src/
payments/
processor.py # public API
_validator.py # implementation detail (prefixed underscore)
tests/
test_payments.py # covers validation *through* processor
conftest.py # shared fixtures & factories
- Tests live under
tests/
, mirroring features rather than files. - Private helpers (e.g.,
_validator.py
) never appear in imports insidetests/
.
Use dataclass factories with optional overrides—type-safe, immutable, and composable.
# src/payments/models.py
from __future__ import annotations
from dataclasses import dataclass
from typing import Literal
Currency = Literal["USD", "EUR", "GBP"]
@dataclass(frozen=True)
class Address:
street: str
city: str
house_number: str
@dataclass(frozen=True)
class PostPaymentRequest:
card_account_id: str
amount: int # cents
currency: Currency
source: Literal["Web", "Mobile"]
last_name: str
date_of_birth: str # ISO8601 YYYY-MM-DD
cvv: str
token: str
address: Address
brand: Literal["Visa", "Mastercard", "Amex"]
# tests/factories.py
from datetime import date
from payments.models import Address, PostPaymentRequest
def make_address(**overrides) -> Address:
defaults = dict(
street="Test Address Line 1",
city="Testville",
house_number="123",
)
return Address(**{**defaults, **overrides})
def make_payment_request(**overrides) -> PostPaymentRequest:
defaults = dict(
card_account_id="1234_5678_9012_3456",
amount=10_00,
currency="USD",
source="Web",
last_name="Doe",
date_of_birth=str(date(1980, 1, 1)),
cvv="123",
token="tok_test",
address=make_address(),
brand="Visa",
)
return PostPaymentRequest(**{**defaults, **overrides})
- Return complete, valid objects with sensible defaults.
- Accept arbitrary keyword overrides—type-checked by
mypy
. - Extract nested factories (
make_address
) as domains grow. - Compose factories rather than duplicating literals.
- For extremely complex graphs, graduate to the Test Data Builder pattern (chained mutator methods returning new frozen instances).
ruff check .
mypy --strict src tests
pytest --cov=src --cov-fail-under=100
python -m pip check
(dependency health)pre-commit run --all-files
(formatters, linters)
All stages must pass before merging to main
.
- Immutable data eliminates an entire class of state-related bugs.
- Strict typing catches integration errors before runtime.
- 100 % behavior coverage ensures refactors are fearless.
- Small, pure functions maximize composability and cognitive clarity.
“If it isn’t proven by a failing test first, it doesn’t exist.” — You, every day
Use this guide as the non-negotiable contract for any Python project you touch.