Designing for the Long Term

Updated

September 8, 2026

Designing for the Long Term

Software lasts when the boundaries are real, the API is small, and the defaults stay boring: functions, dataclasses, the standard library, tests, and a version you can explain. This chapter is a recap you can keep on the desk.

Mental model

A package boundary is a directory other code imports. Inside, you may shuffle files. Across it, names are promises.

A stable API is a handful of functions (and maybe a dataclass) with docstrings. Hidden helpers start with _ or live in a module you do not document.

Long-term Python in this book means: Python 3.14, uv, ruff, pytest, UTC in storage, pathlib, logging, subprocess.run as a list, no secrets in git, wheels from uv build.

Worked examples

Case 1: Two modules, one public function

Save as a tiny package in a temp tree and import it. Production looks the same without TemporaryDirectory.

desk/__init__.py exports label only. desk/format_ticket.py can change.

# stable_desk.py
import sys
from pathlib import Path
from tempfile import TemporaryDirectory


INIT = '''\
from desk.format_ticket import label

__all__ = ["label"]
'''

FORMAT = '''\
def label(ticket_id: int, table: int) -> str:
    """Return a one-line ticket label for the desk display."""
    return f"ticket {ticket_id} → table {table}"
'''


def main() -> None:
    with TemporaryDirectory() as raw:
        root = Path(raw)
        pkg = root / "desk"
        pkg.mkdir()
        (pkg / "__init__.py").write_text(INIT, encoding="utf-8")
        (pkg / "format_ticket.py").write_text(FORMAT, encoding="utf-8")
        sys.path.insert(0, str(root))
        import desk

        print(desk.label(7, 12))
        print(desk.__all__)


if __name__ == "__main__":
    main()

Run:

uv run python stable_desk.py

Output:

ticket 7 → table 12
['label']

Callers import desk.label. They do not import desk.format_ticket unless you say so.

Case 2: pyproject.toml is the project boundary

The long-term file at the root of a library:

[project]
name = "desk"
version = "1.0.0"
description = "Labels and shifts for a small desk."
requires-python = ">=3.14"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

That file, desk/, tests, and README.md are the repo. Everything else is extra.

Case 3: One process that still looks like the first chapter

Save as desk_open.py. A function, a print, a main guard. Types where they help. No framework.

# desk_open.py
def open_table(n: int) -> str:
    if n <= 0:
        raise ValueError(f"table {n}: number must be positive")
    return f"opened table {n}"


def main() -> None:
    print(open_table(12))


if __name__ == "__main__":
    main()

Run:

uv run python desk_open.py

Output:

opened table 12

If this is enough, stop. Add a package when a second module would otherwise collide.

The trap

A “platform” on day one: a base class, a plugin registry, and a settings object for a label.

Save as too_much.py:

# too_much.py
class BaseLabel:
    def render(self, ticket_id: int, table: int) -> str:
        raise NotImplementedError


class DeskLabel(BaseLabel):
    def render(self, ticket_id: int, table: int) -> str:
        return f"ticket {ticket_id} → table {table}"


def main() -> None:
    print(DeskLabel().render(7, 12))


if __name__ == "__main__":
    main()

Run:

uv run python too_much.py

Output:

ticket 7 → table 12

The boring package from Case 1 prints the same string with one function. Add a class when state lives and behavior belongs to it. Add plugins when you have a second implementation, not a slogan.

The boring rule

  • Export a small __all__. Keep helpers behind it.
  • Store UTC, log with logging, run children with subprocess.run([...]), read files with Path.
  • Test the public functions. Lint with ruff. Audit with uv audit.
  • Version in pyproject.toml. Build with uv build.
  • Reflection and FFI stay at the edges.
  • When in doubt, write the function from Case 3.

Try this

  1. In stable_desk.py, add desk/shifts.py with shift_label(name) and export it from __init__.py.
  2. In desk_open.py, catch ValueError for open_table(0) and print the error.
  3. Delete BaseLabel from too_much.py and keep a single label function.