Reusable design
Reduce duplication carefully and choose functions or objects based on state and variation.
Reuse is valuable when it captures a stable concept. Premature abstraction couples code that only looks similar today. The goal is not the fewest lines; it is the smallest design that keeps likely changes safe and understandable.
Understand duplication first
Duplicated knowledge is more dangerous than duplicated syntax. Two tax calculations that must follow the same regulation should share one policy. Two loops that happen to look alike but serve different rules may deserve to remain separate.
Use this sequence:
- Observe at least two concrete cases.
- Identify what must change together.
- Name the shared concept in domain language.
- Extract the smallest useful interface.
- Keep variation explicit through arguments or injected behavior.
- Remove the abstraction if callers become harder to understand.
Extract policy, not plumbing
from collections.abc import Iterable
from decimal import Decimal
def subtotal(prices: Iterable[Decimal]) -> Decimal:
return sum(prices, start=Decimal("0"))
def total_with_tax(prices: Iterable[Decimal], tax_rate: Decimal) -> Decimal:
amount = subtotal(prices)
return amount + amount * tax_rateThe names express business concepts. A generic helper such as
calculate(items, mode, include_extra, factor) would be more flexible but less coherent.
Prefer pure functions for transformations
A pure function returns the same result for the same arguments and does not mutate external state. It is easy to test, compose, and run concurrently.
from dataclasses import dataclass
@dataclass(frozen=True)
class LineItem:
unit_price_cents: int
quantity: int
def line_total_cents(item: LineItem) -> int:
if item.quantity < 0:
raise ValueError("Quantity cannot be negative")
return item.unit_price_cents * item.quantityUse functions when behavior is naturally stateless, variation can be passed explicitly, and there is no lifecycle to protect.
Use classes to protect state and lifecycle
class Batch:
def __init__(self, capacity: int) -> None:
if capacity <= 0:
raise ValueError("Capacity must be positive")
self._capacity = capacity
self._allocated = 0
@property
def remaining(self) -> int:
return self._capacity - self._allocated
def allocate(self, quantity: int) -> None:
if quantity <= 0:
raise ValueError("Quantity must be positive")
if quantity > self.remaining:
raise ValueError("Allocation exceeds remaining capacity")
self._allocated += quantityThe class owns an invariant: allocated quantity cannot exceed capacity. Exposing setters for both fields would weaken that guarantee.
Compose behavior with protocols
Inheritance is useful for genuine substitutability, not merely code sharing. Prefer a protocol when callers need a capability:
from typing import Protocol
class Notifier(Protocol):
def send(self, recipient: str, message: str) -> None: ...
def notify_failure(notifier: Notifier, recipient: str, job_id: str) -> None:
notifier.send(recipient, f"Job {job_id} failed")Production can use email; tests can use a recording fake. The caller does not inherit from or inspect either implementation.
Avoid code golf
Comprehensions are excellent for a simple map or filter:
normalized = [name.strip().casefold() for name in names if name.strip()]Use an ordinary loop when handling errors, updating several values, or expressing multiple conditions. Avoid metaprogramming, dynamic attribute creation, and nested lambdas unless their benefit is substantial and documented.
Pitfalls
- extracting a helper named after implementation rather than purpose;
- boolean parameters that create hidden modes;
- base classes whose methods raise
NotImplementedErrorfor most subclasses; - service classes containing only unrelated static methods;
- dependency injection containers for a small program;
- mocking every collaborator instead of testing stable public behavior;
- forcing two workflows through one abstraction until both are full of exceptions.
Design checklist
- Shared code represents shared knowledge, not accidental similarity.
- Functions are preferred for stateless transformations.
- Classes protect meaningful invariants or lifecycle.
- Interfaces are narrow and defined near their consumers.
- Composition is considered before inheritance.
- Call sites are clearer after extraction.
- The abstraction can be deleted or changed without a broad rewrite.
Practice
Find three similar functions in a project. Write what each one is allowed to change independently. Extract only the knowledge that must remain synchronized. If you cannot give the abstraction a precise domain name, keep the duplication and revisit it after another use case.
LEARNING RECORD
Finish this lesson
Mark it complete when you can explain the main decision without looking.
KNOWLEDGE CHECK