Python Atlas
Software engineering

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:

  1. Observe at least two concrete cases.
  2. Identify what must change together.
  3. Name the shared concept in domain language.
  4. Extract the smallest useful interface.
  5. Keep variation explicit through arguments or injected behavior.
  6. 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_rate

The 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.quantity

Use 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 += quantity

The 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 NotImplementedError for 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

Reusable design

1Two workflows contain nearly identical loops but follow rules that may change independently. What is the best design choice now?
2When is a class more appropriate than a pure function?