---
paths:
  - "*/src/**/*.py"
  - "*/tests/**/*.py"
---

# Engineering Philosophy

The deeper reasoning behind the codeflash repos' technical choices. Understanding the *why* enables applying the thinking to novel situations, not just pattern-matching on existing code.

## Library Thinking vs Application Thinking

These are libraries. The engineering decisions flow from one question: **what will the user of this code experience?**

A library author thinks backwards from the import statement:

```python
# This is where design starts
import svcs
registry = svcs.Registry()
```

Then works inward: what does `Registry` need? That goes in `_core.py`. What can go wrong? That goes in `exceptions.py`. What types does the user need? Those get re-exported from `__init__.py`. Everything else is hidden.

This applies even when you're not writing a published library. Every module boundary is a library boundary. Every function signature is an API. The discipline scales down.

## Why Immutability

Mutable shared state is the root cause of an entire class of bugs: race conditions, action-at-a-distance, ordering dependencies, reentrancy hazards. Frozen objects eliminate all of these by construction.

The cost is allocations. `bind()` creates a new object instead of mutating. In Python, this cost is negligible compared to the bugs it prevents. The GC handles short-lived objects well.

The deeper insight: immutability makes code **predictable**. When you see a frozen object, you know its state won't change between the line you're reading and the line below. That's a powerful reasoning tool.

When to break the rule: objects with lifecycle (registries, connection pools, containers). These *need* state transitions. Use `@attrs.define` and make the mutation methods part of the documented interface.

## Why attrs Over dataclasses

attrs predates dataclasses and remains strictly more capable:

- **Validators** at construction time (not just post-init hacks)
- **Frozen + slots** in one decorator (`@attrs.frozen`)
- **Private attribute aliases** (`attrs.field(alias="public_name")` for `_private_attr`)
- **Factory defaults** that actually work (`attrs.Factory(list)`)
- **Composable validators** (`attrs.validators.and_`, `attrs.validators.optional`)

dataclasses are fine for simple structs. When your data has invariants, validation, or privacy needs, attrs is the right tool.

## Why 79 Characters

This isn't nostalgia. It's ergonomics.

- Side-by-side diffs in a code review fit without horizontal scrolling
- Three-pane merge tools work without wrapping
- Terminal-based workflows (ssh, tmux splits) remain usable
- Shorter lines force decomposition — long lines often mean too much is happening

The 88-character default from Black was a compromise for codebases that were already wide. Starting at 79 is starting with discipline.

## Why ruff ALL + Ignores

Most projects pick rules one by one. This is backwards. You miss rules you didn't know existed.

Starting with `ALL` and ignoring what doesn't apply means:
- New rules in future ruff versions are automatically enabled
- You're forced to justify every exception (the ignore comment is documentation)
- Coverage is comprehensive by default, not aspirational

The ignore list is a design document. Each entry says "we considered this rule and rejected it for this reason." That's more valuable than a hand-picked enable list.

## Why 100% Docstring Coverage

`interrogate --fail-under=100` forces every public symbol to have a docstring. This isn't busywork — it's a design pressure.

When you can't write a clear one-sentence docstring for a function, the function is probably doing too much or is poorly named. The docstring requirement surfaces design problems early.

It also means users can always `help()` any public symbol and get something useful. For a library, that's table stakes.

## Why Strict mypy

`strict = true` enables every check mypy has. The pain is upfront (annotating everything, handling edge cases). The payoff is continuous:

- Refactoring is safe. mypy catches breakage across module boundaries.
- APIs are self-documenting. The signature tells you what goes in and comes out.
- `Any` becomes a deliberate escape hatch, not an invisible default.

The version-conditional import pattern (`if sys.version_info >= (3, 11)`) exists because strict mypy demands it. `try/except ImportError` is too loose — it hides real import failures behind the version-gate.

## Why Private Modules

Every module in `src/package/_core.py` starts with underscore. This communicates:

1. **To users**: don't import from here. Use the package-level imports.
2. **To maintainers**: this can be refactored freely. No external code depends on the module path.
3. **To tools**: IDE autocompletion won't suggest `package._core.Thing` when `package.Thing` is available.

The `__init__.py` re-export is the load-bearing interface. The private modules are the implementation that can be split, merged, renamed, or reorganized without breaking anyone.

Exception: `exceptions.py` has no underscore because exception classes are part of the public API by nature — users need them in `except` clauses and imports.

## Why Minimal Dependencies

Every dependency you add:
- Can break with an update
- Can be abandoned by its maintainer
- Increases install time and size
- Adds to your security surface area
- Can conflict with other packages in the user's environment

For a library, this matters more than for an application. Your dependency becomes your user's transitive dependency. They didn't choose it and they can't easily remove it.

The test: does this dependency do something that would take 200+ lines to replicate? If yes, depend on it. If you'd just be wrapping a 20-line function, write the 20 lines.

## Why uv_build

`uv_build` is the build backend for all packages in this workspace:
- Standards-compliant (PEP 621 metadata)
- Fast and minimal — no setuptools baggage
- Consistent with uv as the single tool for all Python operations (run, sync, lock, build)

Version is declared in `pyproject.toml` directly. No dynamic version derivation.

## Design Pressure as a Tool

Many of these choices create *design pressure* — they make bad design uncomfortable:

- **79 chars** pressures you to write shorter expressions, extract functions, use better names
- **100% docstrings** pressures you to make every public symbol worth documenting
- **Strict mypy** pressures you to design clean interfaces with clear types
- **Frozen classes** pressure you to think about state ownership upfront
- **Private modules** pressure you to think about what's truly public

The constraints aren't obstacles. They're tools that push design toward clarity.

## On Simplicity

The goal isn't cleverness. The goal is code that a maintainer (including future-you) can read at 2am during an incident and understand immediately.

- No metaclass magic unless the alternative is worse
- No decorator stacking beyond two deep
- No dynamic attribute generation
- Prefer boring, explicit code over elegant, implicit code

The best code is code that looks obvious in retrospect.

## Testing Philosophy

Tests serve three audiences:

1. **The CI system**: does it work?
2. **Future maintainers**: what is this supposed to do?
3. **Users reading tests as examples**: how do I use this?

The docstring on each test method serves audiences 2 and 3. The assertion serves audience 1. Writing `"""bind() returns a new BoundLogger with merged context."""` is more valuable than `test_bind_returns_new_instance` alone.

Class-based test organization mirrors the code structure. `TestRegistry` contains all tests for `Registry`. This makes it trivial to find the tests for a given class, and to understand the full behavioral surface.

## On Framework Integrations

Framework integrations (FastAPI, Flask, Starlette) live in separate modules, not in core:

```
svcs/
    _core.py      # Framework-agnostic core
    fastapi.py    # FastAPI integration
    flask.py      # Flask integration
    starlette.py  # Starlette integration
```

This keeps the core free of framework dependencies. A Flask user never imports FastAPI code. The integration module adapts the core to the framework's conventions (dependency injection, request lifecycle, etc.) without polluting the core API.

This pattern applies broadly: keep the engine separate from the interface. The core should work without any framework. The integration is a thin adapter.
