agentleFS
AleksandarBisevacAleksandarBisevac/claude-plugins/.claude/skills/writing-python/SKILL.md

writing-python

Skill4 starsChanged 3 months agoMITView on GitHub

How Python is written in this repo — free functions over classes, structured returns instead of mutated arguments, comprehensions, no module state, and the stdlib-only, annotation-free, Python 3.8 dialect the AST lints enforce. Covers modular structure and the import-layer rule, DRY without copying a helper into a second file, what makes a function testable from a `--selftest`, readability rules (guard clauses, named predicates, why-not-what docstrings), and the anti-patterns that have actually bitten here. Use when planning, writing, reading, reviewing or refactoring any `.py` under plugins/audit/, when adding a helper or a new script, when deciding where a function belongs, or when the same logic is about to exist in two places.

2 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

8 rules. About 0 are common, 5 are distinctive, and 3 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈5

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Not a general Python style guide.

    line 8
  2. Distinctive:

    Comprehensions over accumulate loops: when the result is a list or dict and the body is one expression.

    line 45
  3. Distinctive:

    If two modules need the same logic, it belongs in a lower layer both can import — not copied into the second file, and not re-derived.

    line 77
  4. Distinctive:

    When a value must exist in two languages, pin the agreement with a case rather than a comment.

    line 84
  5. Distinctive:

    Do not extract for its own sake.

    line 86
  6. Distinctive:

    Keep functions short enough to hold.

    line 100
  7. Very specific:

    Use None and build inside.

    line 106
  8. Very specific:

    x = x or default when 0, False or "" are meaningful: Use x if x is not None else default.

    line 107

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Writing Python here

Not a general Python style guide. This repo bans things most guides mandate, and the bans are read out of the AST rather than trusted — _output_lints.house_style_violations() fails the build, it does not warn. Everything below is either enforced or measured from the tree as it stands.

The dialect, in one paragraph

Stdlib only. Python 3.8 floor. No typing, no dataclasses, no annotations, no walrus, no from __future__ — banned by AST, because hooks start on every tool call and the import and parse cost is real. %-style formatting; there is not one f-string in the tree. Reach for os.path, json, re, subprocess — not for a dependency.

3.8 also rules out things that read as ordinary today: no X | Y unions, no match, no dict1 | dict2 merge, no list[str]. vermin -t=3.8- catches these; it will not catch the banned imports, which is why the AST lint exists alongside it.

Free functions, structured returns

The measured shape of this codebase is top-level functions by the hundred against a handful of classes, comprehensions everywhere, and global statements you can count on one hand. The ratio is the pattern to keep; the figures are not, so re-derive them rather than trusting a sentence:

grep -rhc '^def ' plugins/audit/scripts plugins/audit/hooks --include='*.py' | paste -sd+ | bc
grep -rn '^class \|^ *global ' plugins/audit/scripts plugins/audit/hooks --include='*.py' | wc -l
  • Write a function, not a class. A class here needs a reason you can say out loud — the six that exist are all genuine (a lock, a launcher). Grouping related functions is what a module prefix is for.
  • Return a new value; do not mutate an argument. 181 functions return a dict or tuple. A function that edits the caller's dict in place and also returns it is two contracts and the caller will rely on the wrong one.
  • No module state. Constants at module level are fine (52 of them, uppercase, frozen tables). Mutable module state that a function writes to is how two callers start disagreeing about the same run. Where a cache is genuinely needed the existing pattern is an explicit _cache/cache=True argument, not a hidden global.
  • Comprehensions over accumulate loops when the result is a list or dict and the body is one expression. Past that, a named function reads better than a nested comprehension.
  • Push I/O to the edges. A function that takes values and returns values can be tested by a test case with no temp directory and no git init; one that reads a file and prints cannot. Split the read from the decision — decide() taking parsed input is the shape every hook already uses, and it is why their logic has cases at all.

Where code lives

  • Name by role. _underscore.py is an importable helper; hyphen-name.py is an entry point something invokes. The name is the contract.
  • A basename must be unique across all of scripts/. A file may sit in a subdirectory — every scanner walks recursively — but import, _loader and _deps.LAYERS all resolve by basename, so two files sharing one would be a single node in the layer graph wearing both files' edges. layer_violations() fails the build by name on it. Directories are labels, not namespaces.
  • Never derive a directory from your own __file__. _output.py is the anchor: SCRIPTS_DIR, PLUGIN_ROOT, HOOKS_DIR, TESTS_DIR, LINTS_DIR, REPO_ROOT. The pinned PATH_PREAMBLE is the only place under scripts/ allowed to touch __file__ at all, and depth_sensitive_paths() enforces it — the rule is "none", not "not a parent", because the sites it replaced were written as a two-step that any nesting-only lint waves through.
  • Imports go down, never sideways or up. _deps.LAYERS assigns every module a layer and layer_violations() fails an unplaced or wrongly-layered file by name. hooks/ may import nothing from scripts/ at all. If a new helper does not fit a layer, that is information about the design, not an obstacle to route around.
  • Section markers every 400 lines. Past that a flat scroll stops being a map; navigability_violations() enforces it for .py; the same rule for the front-end assets is author-enforced.

DRY, and the way it fails here

The rule is not "never repeat a line". It is one fact, one home.

  • If two modules need the same logic, it belongs in a lower layer both can import — not copied into the second file, and not re-derived.
  • The cautionary tale is real and still in the tree: the token formatter exists in Python and is hand-written again in each of the two front-end surfaces. Two of the three now disagree — uTok(2.6) rounds to "3" while fmtTokens(2.6, 1) truncates to "2" — and both carry a comment claiming to mirror the Python. A copy with a comment saying it is a copy is still a copy.
  • When a value must exist in two languages, pin the agreement with a case rather than a comment. A claim that two implementations agree is testable; test it.
  • Do not extract for its own sake. Two similar-looking blocks that answer different questions should stay apart — the wrong abstraction costs more than the duplication.

Readability

  • Guard clauses over nesting. Return early; do not build an arrow of if/else.
  • Name the predicate. if _is_ready(task): beats an inlined three-term and chain, and the name is where the reasoning goes.
  • Module docstring says why the file exists, not what it does. That is a hard rule here and the reason the tree is navigable at 42k lines.
  • Comment the non-obvious constraint, never the mechanism. The house comments that earn their place read like postmortems: what broke, and what would break again. A comment restating the next line is noise.
  • No *Utils, *Helper, *Manager, *Impl. Name a module for the thing it owns.
  • Keep functions short enough to hold. 39 functions in the tree exceed McCabe 15 — that is the known debt, not the target.

Anti-patterns and pitfalls

  • Mutable default arguments (def f(items=[])) — the default is created once and shared. Use None and build inside.
  • x = x or default when 0, False or "" are meaningful. Use x if x is not None else default. This is the single most common silent-corruption bug in code that reduces data to a number.
  • Bare except: or a broad except Exception: that returns a normal-shaped value. Either recover meaningfully or make the failure visible. See no-silent-pass.
  • Iterating a set into serialized output — the order varies per process and the real diff drowns in noise. Impose a total order first.
  • Catching an exception to skip an item, with no count and no name. Silent skips are how a job reports success over half the work.
  • Reaching around the layer rule by importing inside a function to dodge the lint. The lint reads the AST; more importantly, the layering is the design.
  • Adding a file without its five obligations — a tests/test_<name>.py, safe_stdio() first in __main__, a _deps.LAYERS entry, a PLUGIN-BUILD-GUIDE.md tree line and section, and (under scripts/) _output_lints.PATH_PREAMBLE pasted byte for byte above the first sibling import. Four of the five fail CI by name.

Testability

A suite in tests/ is not an afterthought here, and the sweep's coverage verdict fails a module without one. Design for it:

  • a function whose inputs are arguments and whose output is a return value needs no fixture;
  • where a fixture is unavoidable, build it in a tempfile.mkdtemp() and delete it in finally — never commit one;
  • new behaviour means new cases, and a case is only trusted once it has been seen red.

no-silent-pass owns that discipline — mutate the fix, mutate it the other way too, count rather than assert presence, and choose fixture values that tell the two versions apart.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.