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-bddorbehave - 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 testspytest --cov=src --cov-fail-under=100python -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.