| name | code-structure |
| description | Language-agnostic structural craft -- decompose on responsibility not size, prefer deep modules over shallow piles, and shape cohesion, coupling, interfaces, error contracts, and data invariants. Sibling to code-hygiene and readable-code. |
| when_to_use | Writing or reviewing code -- shaping function and module boundaries, interfaces, error contracts, and data types once code-hygiene has removed the outright liabilities. Local reading (control flow, naming, reading order) is readable-code. |
Code Structure
code-hygiene deletes the liability; this skill shapes the units that remain. Four owners, one test:
- hygiene = DELETE what git or a library already owns -- a restating comment, dead code, a name that lies or says nothing. A rule that says delete X is hygiene's.
- code-structure = SHAPE THE UNITS -- how the code is decomposed, how units couple, how an interface reads, how a failure reaches a caller, how data carries its own invariants. Everything here assumes the code should exist; the question is what shape its units and boundaries take.
- readable-code = READ -- how a single body reads line by line: control-flow shape, naming across a set, reading order and working set. Anything about how one body reads rather than how the units are carved is readable-code's.
- pythonica = Python mechanics -- a rule that names a Python construct (
dataclass, NewType, a context manager, match) is pythonica's; keep this skill language-agnostic.
Read code-hygiene first: shaping code that should have been deleted is wasted work. When a fix here would be self-documenting code (a revealing name, an extracted step, an explaining variable), that is exactly what hygiene's "prefer self-documenting code over a comment" meta-rule points to -- hygiene removes the comment, this skill supplies the structure. Where a smell reads as "there is redundant knowledge here", check the boundary: identical text in two places is hygiene's duplication rule; the same decision in two unlike forms is this skill's (DRY-as-knowledge, below).
Every rule below is a recognizable smell (something a reader or author can actually spot) -> the principle it violates -> a fix direction. No line, parameter, or nesting-depth count gates any rule here: that false precision is the size-fallacy this skill exists to replace. (Illustrative counts like "a two-line loop index" still appear -- they describe an example, they do not set a threshold.)
Decomposition and module depth
The stance (resolving the size tension). The oldest fight in this space is Martin ("small functions, extract till you drop, one thing per unit") versus Ousterhout ("deep modules, depth over count, beware classitis and shallow conjoined methods"). This skill takes a side. Decompose on responsibility and cohesion seams, never on size -- size is not the defect, doing-two-things is. Prefer depth (a narrow interface hiding real work) over a pile of shallow one-line methods, because every unit you add is an interface the reader must learn, paid forever. And stop extracting the moment the pieces are more coupled apart than together: if you must read the extracted helper to understand its caller, it was one thought -- leave it whole. The rest of this family is that stance applied.
One caveat scopes the whole family: a name introduced within a body -- an explaining variable or a local named predicate -- costs no caller an interface and is never what "too many units" warns against; only an extracted unit a caller must learn (a method, a module) is counted by the depth-over-count rule. So "name the subexpression" (in readable-code) and "beware the shallow swarm" never actually collide.
- Cannot name it without "and" -- SMELL: to name the unit you reach for a conjunction (
and, or, then) or a vague verb (handle, process, manage, do), and the honest name promises less than the body delivers, so a reader must scan the whole body to learn what a call does. PRINCIPLE: a unit does one nameable thing; the conjunction you reached for is the seam between two responsibilities. FIX: split where the and falls so each piece earns a conjunction-free name -- but stop when the halves are more coupled apart than together.
- Interface almost as wide as the implementation -- SMELL: you read the signature and still must read the body to call it safely; the parameters, exceptions, and ordering rules a caller must learn nearly equal the code the unit saves (a
parse(text, mode, strict, encoding, on_error) that saves the caller five lines but forces them to learn five knobs). PRINCIPLE: modules should be deep -- a narrow interface concealing substantial work; the interface is a cost every caller pays, the implementation is paid once. Depth, not method or class count, measures good decomposition. FIX: widen what the module does behind the same narrow interface, or fold a shallow layer into its neighbour. Judge a split by whether the interfaces got simpler, not whether the pieces got smaller.
- A swarm of shallow one-liners -- SMELL: to follow one operation you open file after file, ping-ponging across single-use helpers that mean nothing except in the order the original caller invoked them (
_step1_load, _step2_massage, _step3_emit, each called once, each reaching into the last one's state). Each cut added an interface while hiding almost nothing. PRINCIPLE: more, smaller components is not automatically better; a cut that leaves two pieces you cannot understand apart added cost with no benefit. FIX: merge units that only ever appear together and reach into each other's state back into one deeper unit. This is the direct counter to "extract till you drop". (Contrast reordering: a single-caller helper that is a genuine, independently nameable unit is merely misplaced, not shallow -- move it, do not merge it. See the "Scattered pieces of one thought" rule in readable-code.)
- Pass-through / middle man -- SMELL: a unit forwards its arguments to another with nearly the same signature, or a class exists only to hold one method that delegates elsewhere --
def save(u): return self._store.save(u). A thin wrapper that adds a name and a hop but makes no decision and holds no data. PRINCIPLE: indirection must earn its keep by hiding real work; a pure pass-through is depth-zero -- full interface cost, no hidden substance. FIX: inline it into its caller, or let clients reach the real object directly; re-extract only when a layer genuinely adds an abstraction. (Boundary: unlike hygiene's orphaned abstraction -- a wrapper with no live caller, which is dead code to delete -- a middle man has real callers; it is not a liability to remove but depth-zero indirection to reshape.)
- Altitude jumps mid-body -- SMELL: reading top to bottom, a call to
charge_customer() sits a line or two from raw index arithmetic; you keep switching between what the code intends and how a detail works, inside one body. PRINCIPLE: a function reads best at one altitude, as a paragraph of intent with the next level of detail one call down (the step-down rule) -- but altitude is a proxy; the real target is a responsibility seam, not uniform elevation. FIX: treat an altitude jump as a prompt to look for a hidden seam -- extract the low-level mechanics into a named helper only when that block is an independently nameable operation; equalising altitude is not itself a reason to cut. Where no such seam exists, leave the computation whole rather than shatter it into one-liners.
Cohesion and change locality
One reason to change. Grouping is a cohesion property, never a size limit.
- Same file in unrelated pull requests -- SMELL: the file lands in a pricing tweak one week and an export-format tweak the next, and the two edits sit in regions that never touch. PRINCIPLE: a module should have one axis of change; two unrelated reasons to edit it means two responsibilities cohabiting. FIX: split along the axis of change so each module answers to one kind of change -- gather what changes together, separate what changes for different reasons.
- One change, scattered edits -- SMELL: adding a payment method or renaming a status forces the same small edit across many files that share no text, and it is easy to miss one, so the change survives only as a map you carry in your head. PRINCIPLE: what changes together should live together; one concept smeared across modules turns one logical change into many physical ones with silent gaps. FIX: gather the scattered pieces behind one owner, or invert the dependency so the knowledge lives where it is used.
- A design decision known in two places (information leakage) -- SMELL: two modules that never share a line of text nonetheless both encode the same design decision -- a wire format, an on-disk layout, a status vocabulary, a fixed sequence of protocol steps -- so a change to that decision must be made in both, and no compiler links them. PRINCIPLE: a single design decision should be known to exactly one module; a decision reflected in several is leaked, and leakage is a leading cause of shallow, entangled modules. FIX: give the decision one owning module and route the others through it. Distinguish from DRY-as-knowledge below (the same decision copied in unlike text a search could in principle find) and from temporal decomposition below (whose reader-lives-with-writer cure is leakage removed): here the tell is two modules that must both know one fact, with nothing duplicated to grep for.
- Modules named after execution phases -- SMELL:
Reader then Processor then Writer, or Setup / Run / Teardown, each holding a slice of the same subject, so any change to that subject touches all of them. PRINCIPLE: decompose by knowledge and responsibility, not by order of execution; temporal order is a runtime fact, not a design boundary. FIX: regroup so each module owns one subject end to end (the code that reads a format lives with the code that writes it). When tempted to split by time-order, ask what distinct knowledge each part would hide -- if none, do not split there.
- One decision, many unlike forms (DRY as knowledge) -- SMELL: to change one rule you edit several places that look nothing alike -- a limit set in a schema and re-checked in a handler, a tax rate written in code and again in a calculation elsewhere -- so a text search will not find them all and one silently keeps the old value. PRINCIPLE: DRY is about knowledge, not text; every decision should have one authoritative representation. Because these copies share no text, hygiene's collapse-the-duplicate rule never flags them -- this is why the rule lives here. FIX: give the decision one owner and derive the rest (generate, compute, reference). Ask: if this rule changed, how many places would I have to find? The answer should be one.
- The flag-ridden shared helper (the wrong abstraction) -- SMELL: a helper once extracted from duplication now sprouts boolean parameters and
if-branches, each added by a caller whose need diverged; to read it for your case you mentally strip the branches that do not apply. PRINCIPLE: a little duplication is cheaper than the wrong abstraction -- two things identical today but changing for different reasons are not one piece of knowledge, and merging them couples independent decisions. FIX: inline the helper back into its callers, then re-extract only the parts genuinely identical and changing for the same reason. Decouple in preference to de-duplicating; unify only what encodes one decision.
Coupling and dependency direction
How units bind to each other, and which way the arrows point.
- Train wreck / message chain -- SMELL: a call walks through objects the caller should not know about --
order.getCustomer().getAddress().getZip() -- so it depends on the internal shape of things two and three hops away. PRINCIPLE: talk only to your immediate collaborators; reaching through a path welds you to internals you never asked for. The smell is reaching through, not a dot count. FIX: give the nearest collaborator a method that returns the end result (order.shipping_zip()) and hide the delegate. Caveat: a chain over a behavior-free data structure (a plain DTO) is not a Demeter violation -- the law is about behavior-rich objects.
- Feature envy -- SMELL: a function keeps pulling three fields off
order to compute something while barely touching the object it lives on; its center of gravity is elsewhere. PRINCIPLE: behavior belongs with the data it operates on; splitting the two raises coupling and scatters the next change across both. FIX: move the method to the class whose data it uses, so the caller tells the object what to do instead of interrogating its fields. If only part envies, extract that part first, then move it.
- A local change that reaches into an unrelated concern (lost orthogonality) -- SMELL: a change you expected to be self-contained -- adding a report column, swapping the cache, retiming a job -- forces an edit in a concern that has nothing to do with it, because the two were wired together with no reason to be. PRINCIPLE: independent concerns should vary independently; when two things are orthogonal, a change to one leaves the other untouched. FIX: sever the incidental link -- separate the axes, invert or drop the dependency -- so each concern can move on its own. Distinct from feature envy (behaviour sitting on the wrong data) and from depend-toward-stability below (arrow direction): orthogonality is about two things that should not touch at all.
- Invisible agreement across a distance -- SMELL: two places in different modules must agree on something unstated -- the order of positional arguments, the meaning of a magic return code, a required call sequence -- so changing one silently breaks the other. PRINCIPLE: the stronger the coupling (position, meaning, execution order), the more it must be kept local; strong coupling across a distance is the costly kind. FIX: weaken the agreement (name the positional arguments so position stops carrying meaning, give a magic return code a named result type) or pull the coupled parties into one unit so the agreement is local and visible. (A bare number's meaning is hygiene's magic-value rule; here the defect is the locality -- an agreement stretched across a distance -- not the literal itself.)
- Stable policy names a volatile detail -- SMELL: a module that embodies durable policy references, by name, a thing that churns underneath it -- a specific pricing table, an experiment flag, a concrete class whose internals change -- so every change to the volatile detail ripples up into the stable policy, and you can trace the blast radius by following the imports. PRINCIPLE: depend in the direction of stability and abstraction; policy should not name the details that change beneath it. FIX: invert the reference so the volatile detail implements an interface the stable module owns -- then its churn stops rippling out. (This is the dependency-direction reading. The sibling case where the volatile detail is an external library named across many modules is "An external type named everywhere" below -- same wrapping move, but that rule owns confining a boundary to one seam, this one owns which way the arrow points. Scope: unit-to-unit dependency reading, not large-scale layering.)
Interface and contract design
The interface is everything a caller must learn to use the module; keep it simpler than the work it fronts (why that matters is the depth resolution above).
- Complexity pushed onto the caller -- SMELL: callers repeatedly supply what the module already knows (a buffer size, retry count, cache policy no caller can choose better than the author), or special-case the same edge at every site, or dispatch on a raw error state the module could have settled. The identical boilerplate reappears at every call site. PRINCIPLE: a value the module can compute internally should not be a parameter; a decision should not be pushed onto callers who know less than you do. FIX: pull complexity downward -- default or auto-tune the value, resolve the edge internally, return a directly usable result. Expose a knob only where callers genuinely hold context the module lacks.
- A call that both acts and answers -- SMELL:
if (set(attr, val)) -- one call changes state and returns a value you branch on, so you cannot tell what the return means and cannot re-ask without re-triggering the effect. PRINCIPLE: command-query separation -- a function either does something (command, returns nothing) or answers something (query, changes nothing), never both; asking a question must not change the answer. FIX: split the mutation from the question. Where a fused form is a deliberate idiom (stack.pop()), confine and name it so the mutation is expected, not ambient.
- Flag argument -- SMELL: a bare boolean or literal steers which of two essentially different bodies runs --
render(doc, true), save(user, false) -- and you cannot read the call without opening the callee. PRINCIPLE: a flag argument confesses the function does more than one thing and shoves the branch onto every caller. FIX: split into two intention-named entry points (render_draft / render_final); if the flag is genuine data, pass a named type, never a raw literal. Bounded by depth: split only where the two paths are genuinely distinct, not into two shallow near-duplicates sharing a hidden seam.
- Easy to misuse -- SMELL: the most natural way to call it is the wrong way -- two adjacent same-typed parameters invite silent transposition, a returned resource must be remembered-and-closed, the convenient overload quietly does the unsafe thing. Correct use requires reading the docs; incorrect use compiles and runs. PRINCIPLE: make the interface easy to use correctly and hard to use incorrectly -- the safe path should be the default, the dangerous path hard to express. FIX: reshape so misuse will not type-check (distinct types for adjacent parameters), scope-bind resources that release themselves, offer no unsafe form to reach for.
- A general mechanism wired to one caller -- SMELL: a generic buffer that knows about the single UI action using it, or the common path forced to understand a rare feature just to do the ordinary thing. The "reusable" piece in fact serves exactly one caller. PRINCIPLE: special-case policy entangled with a general mechanism breeds dependencies and yields a shallow, single-use interface; a somewhat general-purpose interface is usually both simpler and deeper. FIX: separate mechanism from policy -- lift the special cases into the caller that needs them, design for the class of needs. Aim for the simplest interface covering all current uses; adding a knob no caller uses yet is hygiene's speculative generality, to delete, not shape.
- Inconsistent sibling contracts -- SMELL: sibling operations report the same situation in different shapes -- one lookup returns null for "not found", its neighbor throws, a third returns an empty sentinel; one path returns a list, another null, another raises. Every caller special-cases the exact variant it touches. PRINCIPLE: a consistent contract lets a reader predict the unseen from the seen and removes a decision at every call site. FIX: pick one convention for the family and hold it, so a reader who has seen one sibling has seen them all.
- Output argument -- SMELL:
append_footer(report) -- a function's real output is a mutation of one of its arguments, so the effect is invisible at the call site, which reads like a query that changes nothing. PRINCIPLE: arguments are read as inputs; using one as an output channel hides the actual effect. FIX: return the changed value, or make the change an obvious operation on the object that owns the state (report.append_footer()), so the effect is visible where it is invoked.
- A pure-looking routine with hidden effects -- SMELL: a routine's name and signature promise a value or a simple transform, but it quietly does I/O, mutates a shared or global structure, or fires an external effect -- so a reader cannot predict what a call does, and the decision logic cannot be read or tested without its plumbing threaded through it. PRINCIPLE: effects should be visible and pushed to the edges -- keep an effect-free core that reads as intent and confine mutation and I/O to a thin shell, so a call's effects are predictable from its surface. FIX: separate the pure computation from the effectful shell; where an effect is unavoidable, surface it in the return or in the shape of the API, not buried in a routine that looks pure. Two boundaries against hygiene: its misleading-name rule fires only when the name lies about the effect (
get_* that writes -> rename or split), and its GOTCHA comment merely documents an unavoidable effect -- this rule fires whenever effects are threaded through the logic, honest name or not, and the move is to reshape so there is nothing to relabel. Distinct too from command-query separation above, which forbids a single function fusing one command with one query; this isolates effects from logic across a whole body.
Error and edge-case handling
Failure is part of the interface. This family owns the contract -- which situations are errors at all, and how a failure reaches the caller. The mechanics (which exception type, Result versus Option, syntax) defer to pythonica:python-error-handling.
- An error that could have been an ordinary outcome -- SMELL: an interface raises or returns failure for a situation the semantics could absorb -- removing an already-absent key raises, a slice past the end throws instead of clamping, a lookup miss errors where an empty result would do -- and every caller sprouts the matching guard until the guarding outweighs the work. PRINCIPLE: every condition treated as an error is a new path each caller must handle; the best special case is the one the semantics make impossible. FIX: redefine the operation so the former error is an ordinary outcome (deleting an absent key is success; an over-long slice clamps; a miss returns empty). Broaden what counts as ordinary -- never swallow a genuine, unrecoverable fault.
- A failure you can forget to check -- SMELL: a routine reports failure by returning a sentinel, status code, or null the caller must remember to inspect; forgetting compiles clean and passes review, then fails later far from the cause. PRINCIPLE: a condition that is a failure must be impossible to overlook -- separate the error channel from the return value so an unhandled error is loud, not silently dropped. FIX: raise it, or return an explicit result the caller cannot use without unwrapping, so ignoring the error takes a visible, deliberate act. The invariant is un-ignorability; the vehicle is a language choice. (When absence is an ordinary outcome, return the empty result -- that is the rule above, not this one.)
- Handled where it cannot be judged (detect low, handle high) -- SMELL: a routine meets a problem it has no authority to resolve and resolves it anyway -- swallows it, logs and continues, hands back a stand-in -- so the caller who could actually act never learns anything went wrong (a low-level cache read that catches a disk error and quietly returns an empty result, hiding data loss from the request handler that would have retried or aborted). PRINCIPLE: detect an error where you have the context to notice it; handle it where you have the context to decide. Resolving it in between discards information the real decision-maker needed. FIX: propagate the detected error outward carrying enough context for the level that can decide. (A stand-in from a routine with no business deciding is a mislocated handler -- distinct from hygiene's stub-shipped-to-look-complete.)
- Extracting a happy-path helper just to tidy the try -- SMELL:
try/catch is tangled through the logic and you cannot read the normal flow, so you reflexively pull the happy-path body into a helper purely to unclutter the try. PRINCIPLE: an extraction earns its place by depth and cohesion, not tidiness; a happy-path helper that exists only to shrink the try is a shallow conjoined method -- twin halves read only in sequence, more coupled apart than together. FIX: extract only when the pulled-out work is an independently nameable operation the rest of the code could call. When it is not, the clutter is the symptom: the cure is fewer error paths (define the edge away) or handling at the level with authority, not a wrapper that relocates the noise.
Data and type modeling
Push invariants into the shape of the data so they cannot be forgotten. (The principles are language-agnostic; the constructs -- sum types, value objects, newtypes -- defer to pythonica.)
- Primitive obsession / data clump -- SMELL: a domain idea travels as raw primitives -- money as a bare float, a point as two
x, y arguments, a date range as two strings -- and the same little cluster reappears signature after signature, re-validated at every site. PRINCIPLE: a recurring group of primitives, or a primitive carrying its own rules, is a missing type. FIX: introduce the type (Money, DateRange) and pass it whole, moving its guarding rules onto it. Wrap only when rules travel with the value; wrapping every bare primitive manufactures thin, empty wrappers.
- Illegal states are representable -- SMELL: a record can hold combinations that cannot happen -- both an
error and a result field populated at once, or a status that is any free-form string when only a fixed set is legal -- so every reader re-checks the invariant defensively. PRINCIPLE: model data so illegal states cannot be constructed; let the type carry the invariant instead of scattering runtime checks that can be forgotten. FIX: reshape so the impossible combination has no representation -- a variant for the either-or, a bounded type for the constraint. Model the invariants that actually bite, not every conceivable one.
- Validity proven then thrown away -- SMELL: a function re-checks inputs a caller already validated, and validation returns a bool or raises rather than handing back a narrowed value, so the proof of validity is discarded the instant it is established and everyone downstream re-earns it. PRINCIPLE: validate once at the boundary and return a type that carries the proof; downstream code should receive already-parsed data, not re-verify it. FIX: turn the validator into a parser -- on success return the narrowed type (
ValidEmail, a non-empty list) -- so further re-checks are unnecessary.
- Invariant held only by everyone's good behavior -- SMELL: callers set an object's fields directly, so an invariant (
start <= end, balance equals the sum of entries) is preserved only by convention -- you cannot point to the one place it is guaranteed. PRINCIPLE: establish invariants at construction and preserve them across every method; do not expose the representation so outsiders can break it. FIX: make the representation private, enforce the invariant in the constructor, permit mutation only through methods that re-establish it. (Where a parsed value carries the proof, that is the rule above; this one owns hiding the representation across mutation.)
- Half data structure, half object -- SMELL: a type exposes public fields like a DTO and carries business methods like a real object; or a class is nothing but getters and setters while the logic interpreting its fields lives scattered across callers. PRINCIPLE: data/object anti-symmetry -- a thing should be either a data structure (exposes data, no behavior) or an object (exposes behavior, hides data); the hybrid gets the disadvantages of both. FIX: pick a side. A deliberate validated value object or DTO at a serialization or layer boundary is not this smell -- carrying data across a boundary is its whole job.
- A field alive only part of the time -- SMELL: an object has a field meaningful only during one phase of its life -- populated while an algorithm runs, empty before and after -- so the code is dotted with "if this field is set" guards. PRINCIPLE: every field should be meaningful for the object's whole life; a sometimes-valid field is a second object cohabiting the first. FIX: extract the sometimes-field and its code into its own object where it is always valid, or model the empty case explicitly (a special-case / null object) so the guards disappear. (Distinct from mutually-exclusive fields, which want a variant type.)
- The same switch, everywhere -- SMELL: the same
switch or if-cascade on the same type-code recurs site after site; adding one case means finding and editing every one, and a missed site fails silently. PRINCIPLE: one axis of variation should have one home; a type-code fanned out into repeated conditionals scatters a single decision across the codebase. FIX: where the dispatch recurs, move behavior onto the type (polymorphism or a lookup table) so a new case is one new unit. A single localized switch -- especially exhaustive pattern-matching -- is fine; convert only on recurrence, or reflexive subclassing breeds classitis.
- Refused bequest -- SMELL: a subclass inherits methods and fields it does not want -- overriding some to throw or do nothing, ignoring others -- so "is-a" holds in name but not in use. PRINCIPLE: inheritance asserts the subtype honors the supertype's contract; refusing the bequest means the hierarchy models implementation reuse, not a genuine is-a. FIX: replace the inheritance with delegation, or push the refused members down to the siblings that actually want them.
Design for change and the reshaping discipline
Keep the code cheap to reshape, and keep reshaping honest.
- An external type named everywhere -- SMELL: the same vendor or library type is named directly across many modules; the day the library changes a signature or gets swapped, the edit ripples to every site that spoke its name. PRINCIPLE: a boundary should be crossed in exactly one place; a dependency wrapped behind an interface you own confines its churn to a single seam. FIX: depend on a thin adapter phrased in your domain's terms. Keep that adapter deep -- it must absorb the library's awkwardness, not forward calls one-to-one; a shallow pass-through earns nothing and still must be maintained. (Sibling to "Stable policy names a volatile detail" above: that rule owns which way the dependency arrow points; this one owns confining a boundary crossing to a single seam.)
- Choosing the design you cannot back out of -- SMELL: a volatile decision -- a rate, a policy threshold, a vendor choice -- is hard-coded identically at many call sites, so reversing it when the requirement shifts means a codebase-wide sweep, where one seam (a single
tax_rate() function) would have localized it. PRINCIPLE: when correctness does not decide between two workable designs, reversibility should -- the option that is Easier To Change is the better default, because the requirement driving the fork is the thing most likely to move, and the cost of a wrong pick is how many call sites a reversal touches. FIX: prefer the option whose decision lives behind a single seam rather than smeared across every call site. Spend this only on a fork you have real reason to think will move; do not isolate a decision that is already stable.
- Structure and behavior in one diff -- SMELL: one change both reshapes the code and alters what it does, so the diff is unreviewable -- a rename and a behavior change in the same hunk, indistinguishable. PRINCIPLE: structural and behavioral change are verified by opposite means (structure by "behavior is unchanged", behavior by a small focused diff), and each becomes nearly uncheckable once tangled with the other. FIX: tidy first, in its own commit -- pure, behavior-preserving restructuring -- then make the behavior change on the clean structure. Sequence the two; never blend them. (Commit-splitting mechanics defer to your git workflow skill.)
Scope
What this skill deliberately leaves to others, one reason each:
- Comments -- OUT. Wholly owned by
code-hygiene (KEEP/DELETE/EXEMPT and the self-documenting-code meta-rule). The explaining variables and phase seams that meta-rule points to live in readable-code; cross-link, never re-adjudicate a comment.
- Control flow, naming, reading order -- OUT (to
readable-code). How a single body reads line by line -- guard clauses, whole-set naming, locality and working set -- is the local-clarity sibling. This skill carves the units; readable-code reads them.
- Formatting, whitespace, line length, alignment -- OUT. Auto-formatter territory; legislating layout would smuggle in the numeric thresholds this skill's register forbids. The one shaping-relevant piece, phase seams, is a
readable-code rule.
- Error handling -- IN, but only as interface semantics (define errors out of existence, un-ignorability, detect-low/handle-high, do-not-extract-a-shallow-happy-path). Which exception type,
Result versus Option, and syntax defer to pythonica:python-error-handling.
- Effects and state -- IN, as interface honesty (an effect-free core, effects surfaced not buried). Folded into interface-and-contract design above; it is a shaping concern about what a signature promises, not a separate topic.
- Cohesion and decomposition -- IN, and the anchor. The size-versus-depth resolution is invisible to a naive "small functions" framing and to hygiene (which only deletes); without it the skill is just "small functions, good names".
- Data and type modeling -- IN at the principle level (model invariants into the shape so illegal states cannot be built and validity is proven once). The constructs -- newtypes, variants, dataclasses, validation libraries -- are
pythonica.
- Coupling -- IN at the unit/module reading level (Demeter, feature envy, orthogonality, connascence locality, depend-toward-stability). Large-scale layering and hexagonal architecture are out of scope.
- Fail-fast assertions, error-message wording, naming encodings -- OUT. The shaping half of assertions is
readable-code's obscurity rule; message content belongs to logging-patterns and observability; a name implying a wrong type is hygiene's misleading-name.
Sources
Ousterhout, A Philosophy of Software Design (deep modules, information leakage, define errors out of existence, obscurity, mechanism vs policy, pull complexity down); Martin, Clean Code (one thing, CQS, flag/output arguments, data/object anti-symmetry, step-down rule) -- resolved toward Ousterhout on size; Fowler, Refactoring (the smell catalogue and reciprocal moves); Beck, Tidy First? / Implementation Patterns (symmetry, chunking, structural-before-behavioral); Kernighan and Pike, The Practice of Programming (clarity, consistency, detect-low/handle-high); Hunt and Thomas, The Pragmatic Programmer (DRY-as-knowledge, ETC, orthogonality); Metz (the wrong abstraction); King (parse, don't validate); Wlaschin and Minsky (make illegal states unrepresentable); Liskov and Guttag (representation invariants); Meyer (command-query separation); Bloch (API design); Evans (ubiquitous language); Page-Jones (connascence).