python-code-style
Single owner of "what does well-styled Python look like in this stack": ruff (lint + format) and numpydoc docstrings. This skill is explicitly manual — Claude runs ruff on the files it has just touched, no hook involved.
Works with
Agent Skills format with YAML frontmatter. Claude Code reads it as-is.
---
name: "python-code-style"
description: "Single owner of \"what does well-styled Python look like in this stack\": ruff (lint + format) and numpydoc docstrings. This skill is explicitly manual — Claude runs ruff on the files it has just touched, no hook involved."
license: "BSD-3-Clause"
---
# Python Code Style
Single owner of "what does well-styled Python look like in this
stack": ruff (lint + format) and numpydoc docstrings. This skill is
explicitly **manual** — Claude runs ruff on the files it has just
touched, no hook involved.
## Stop conditions — read before anything else
- **Do not configure a PostToolUse / PreToolUse hook for ruff.** This
skill is intentionally manual. A hook tightens the loop in ways
that bite (every micro-edit triggers a fix cycle, partial files
fail D-rule checks mid-write, retries can stall the turn). If the
user explicitly asks for an automated hook later, redirect to
`update-config` — but the default is "Claude runs ruff itself."
- **Do not substitute ruff with `black` / `isort` / `flake8` /
`pydocstyle` / `pylint`.** Ruff is the canonical linter in this
stack (`data-science-python-stack` Tier 1). If `import ruff` /
`pixi run ruff --version` fails, route through `python-env-manager`
to install — don't silently fall back.
- **One fix attempt per file, then surface.** If `ruff check`
reports issues after Claude's first fix, address them once. If the
*same* issue persists after the second pass, stop editing that
file and surface the remaining diagnostics + diff to the user.
This is the anti-infinite-loop guardrail — do not enter a third
cycle on the same warning.
- **Don't lint files outside the user's code.** The hook scope is
`src/<pkg>/`, `experiments/`, `audit/`, `data/eda.py` (the
explore-ml-data EDA script), top-level `*.py` scripts, and any
package directory the user owns. Skip vendored paths, generated
files, the rest of user-owned `data/`, and anything under `.pixi/`,
`.venv/`, `node_modules/`, etc.
- **Never write `ruff.toml` from memory.** The bundled
`templates/ruff.toml` is the single source of truth — it encodes
the per-file ignores (`experiments/**`), the numpydoc convention,
and the rule selection this stack expects. Initial setup requires
**`Read .agents/skills/python-code-style/templates/ruff.toml`**
*this turn*, then `Write <project-root>/ruff.toml` verbatim from
that file's content. Authoring a custom `ruff.toml` from training-
data memory drops half the contract silently. If you catch
yourself typing `[lint]` / `[format]` / `select = [...]` without
having read the template this turn, STOP and `Read` it first.
- **Don't call `warnings.filterwarnings(...)` unless the user
explicitly asks for it.** Same for `warnings.simplefilter`,
`@pytest.mark.filterwarnings`, and `filterwarnings = [...]` in
`pytest.ini` / `pyproject.toml`. Warnings are signal in this
stack.
- **Documentation describes the problem, not the workflow.** A
committed file's module docstring, header, and comments must
describe the **data-science problem** and the file's role in it —
never the skills, the gates (`G-*`), the cell runner, the run
digest, the journal / backlog / design-note machinery, or "the
process we are following". That guidance is agent-facing and lives
in the skills, not in the user's files. When you touch a file that
now carries **real content**, rewrite any leftover generic template
or workflow prose into concise, problem-specific docs grounded in
the current context (the project goal, the experiment's hypothesis,
the dataset). If the file is still an empty skeleton (no content /
no context yet), leave its placeholder — the contextualization
happens when the content lands. Details: § "Contextualize the
comments".
## Pre-flight — emit this checklist as visible text before running ruff
```
Pre-flight (python-code-style):
- [ ] ruff importable in the project's env (`pixi run ruff --version`
succeeds, per `data-science-python-stack` Tier 1)
- [ ] `ruff.toml` present at project root.
If absent AND stack + workspace are already set up: the
bundled template MUST be read **this turn** before being
written verbatim.
Evidence: Read .agents/skills/python-code-style/templates/ruff.toml
(this turn) + Write <project-root>/ruff.toml (this turn)
| "n/a — ruff.toml already at project root"
**Inline-authored ruff.toml from memory is NOT evidence.**
- [ ] File list ready: <abs paths of .py files touched this turn>
- [ ] Decision recorded: this is the first ruff pass on these files
(proceed) | second pass (proceed but stop on persistent
issues) | third pass on same warning (STOP, surface to user)
- [ ] One-fix-per-file rule acknowledged: max two passes per warning,
then surface remaining diagnostics + diff to the user.
- [ ] Comments contextualized: each touched file with real content has
problem-specific docs and NO workflow/skill/gate/runner/digest
meta (§ "Contextualize the comments")
Evidence: per file, "rewrote header to <problem context>" |
"no leftover template/workflow prose" |
"n/a — empty skeleton, no context yet"
```
## Scope
- **In scope:** running `ruff format` + `ruff check --fix` + `ruff
check` on Python files Claude has just generated or edited;
authoring numpydoc docstrings on public functions and classes;
contextualizing each touched file's comments to the data-science
problem and stripping workflow/process meta (§ "Contextualize the
comments"); dropping the `ruff.toml` template into a fresh project.
- **Out of scope:** type hints (mypy / pyright are not in the
stack); naming conventions ruff doesn't enforce; setting up
PostToolUse / PreToolUse hooks; linting non-Python files.
## What to run, in what order
For every Python file touched this turn (call them `<files>`), run
inside the project's environment manager — `pixi run` for pixi
projects, equivalent for uv / poetry / conda (per
`python-env-manager`):
```bash
pixi run ruff format <files>
pixi run ruff check --fix <files>
pixi run ruff check <files>
```
Three steps, in order:
1. **`ruff format`** — applies the formatter (line length, quoting,
trailing commas, blank lines around defs). Idempotent.
2. **`ruff check --fix`** — auto-fixes everything ruff knows how to
fix in place: import sorting (`I`), legacy syntax (`UP`),
detectable bug patterns (`B`).
3. **`ruff check`** (no `--fix`) — final pass. Anything reported
here needs Claude's attention: missing docstrings (`D`),
undefined names (`F`), code structure issues. Address them, then
re-run the trio. Apply the **one-fix-per-file rule** from Stop
conditions.
If a file under `experiments/` or `audit/`, or the `data/eda.py`
EDA script, has a `D100` ("missing module docstring") or `D103`
("missing function docstring") warning, that's expected for `# %%`
cells; the bundled `ruff.toml` per-file-ignores `D100` + `D103`
(and `E402`, `B018`) for `experiments/**`, `audit/**`, and
`data/eda.py`. If you're seeing them, the `ruff.toml` isn't loaded
— check that it lives at the project root.
Audit files (`audit/<NN>_<short_name>.py`, owned by
`audit-ml-pipeline`) and the EDA script (`data/eda.py`, owned by
`explore-ml-data`) lint the same way as experiment files: same
`# %%` cell convention, same per-file ignores, same NumPyDoc
convention for any helper functions. After writing or editing one of
these files, run the same trio (`ruff format` → `ruff check --fix`
→ `ruff check`).
## Contextualize the comments
ruff makes a file *well-formed*; this pass makes it *well-documented
for the problem*. Templates ship with neutral placeholders and a
little authoring scaffolding so the generating skill knows what each
cell / module is for. None of that should survive into the user's
committed file — the user's files document the **data-science
problem**, not the process that produced them.
After the ruff trio, for every touched file that now carries **real
content**, do a quick documentation pass:
1. **Fill the header for this problem.** Replace any `<placeholder>`
or generic header with a one- or two-line description of what this
file does *here*: the experiment's hypothesis (`experiment.py`),
what this module contributes to the pipeline (`src/<pkg>/*.py`),
which report this file reviews and what it tests
(`audit/<stem>.py`), what the dataset is and what the analysis
looks at (`data/eda.py`). Pull the wording from the live context —
the project goal, the approved design note, the dataset.
2. **Strip the workflow meta.** Delete leftover process commentary:
skill names, gate IDs (`G-*`), `§` cross-references, "the agent",
"the (cell) runner", "the digest", "run cell by cell", journal /
backlog / sourcing jargon, and inline guard-rails like "MUST NOT
call `put` / bare expressions, don't `print`". Those guard-rails
stay enforced — they live in the owning skill's SKILL.md, which is
where the agent reads them, not in the user's file.
3. **Keep the substance.** Genuinely useful problem / engineering
context and the numpydoc docstrings stay. Cell markers (`# %%`)
and any remaining `<...>` placeholders the agent still has to fill
stay until they are filled.
The result should read like a colleague wrote the file for this
project — not like a generated scaffold. Skip a file that is still an
empty skeleton (e.g. a freshly scaffolded `src/<pkg>/*.py` with no
body yet): there is no context to write about until the content
lands, and the contextualization happens on the edit that fills it.
This pass is **owned here** so the rule is enforced uniformly. Every
file-writing skill already hands off to this skill after a write;
that hand-off now also covers contextualizing the comments.
## Numpydoc — the docstring convention
Public functions and classes carry numpydoc-format docstrings; ruff's
`D` rules with `pydocstyle.convention = "numpy"` enforce the shape.
**A bare one-line summary is NOT sufficient for public functions.**
The `Parameters` / `Returns` (and `Raises` when applicable) sections
are mandatory — even when the function is small, even when the user
says "just the summary is fine". Approving a one-line docstring on
a public function silently fails the contract this skill enforces;
the function looks `D`-rule-clean (D100/D103 don't fire) but the
parameter shapes and return type that callers actually need are
missing. Private helpers (`_leading_underscore`) are the only
exception: the default `D` rules allow them to omit docstrings, but
public callable surfaces always carry the full numpydoc shape.
Skeleton:
```python
def predict_price(X, model, *, n_jobs=1):
"""Predict option prices from a feature matrix.
Parameters
----------
X : pandas.DataFrame
Feature matrix with one row per option.
model : sklearn.base.BaseEstimator
Fitted estimator with a ``predict`` method.
n_jobs : int, default=1
Number of parallel jobs.
Returns
-------
numpy.ndarray of shape (n_samples,)
Predicted prices, one per row of ``X``.
"""
```
Conventions worth surfacing because they're non-obvious:
- **One-line summary on the first line**, in the imperative mood
("Predict ..." not "Predicts ..."). No trailing period in the
summary line if D400 is enabled — but in `numpy` convention it
is, so write the period.
- **Blank line between summary and the rest.**
- **Parameter shapes go in the type slot**, not the description, e.g.
`X : ndarray of shape (n_samples, n_features)`.
- **`Returns` section** lists the return value; if there are
multiple returns, list each on its own row. Don't omit the type.
- **Private helpers** (`_leading_underscore`) don't need a docstring
under the default `D` rules — ruff allows that.
- **Modules** (top of file) should start with a one-line summary.
Skipped under `experiments/**` per the bundled `ruff.toml`.
## Initial setup — dropping the `ruff.toml` template
When this skill is invoked on a fresh project that has no
`ruff.toml` at its root **and** the stack + workspace have been
scaffolded by their respective skills:
1. **Read the bundled template** with the file-reading tool *this
turn*:
`Read .agents/skills/python-code-style/templates/ruff.toml`.
The pre-flight Evidence row for the `ruff.toml present` check
requires this read; an inline-authored config from memory does
not satisfy it.
2. **Write the content verbatim** to `<project-root>/ruff.toml`.
No edits, no "improvements", no rule additions. The template
encodes the per-file ignores (`experiments/**`), the
`pydocstyle.convention = "numpy"` setting, and the rule
selection this stack expects. Diverging from it drops half the
contract.
3. **Verify ruff picks it up**: `pixi run ruff check --show-settings
.` should report the `numpy` convention and the `select` list
from the template.
Do not fold ruff config into `pyproject.toml` automatically — the
project may not have one, or the user may prefer a separate file.
The standalone `ruff.toml` is unambiguous.
**Forbidden shortcuts:**
| Shortcut | Why it's wrong |
|---|---|
| "I know what ruff.toml should contain" → author from memory | The bundled template carries `experiments/**` per-file ignores, the numpydoc convention, and a curated rule selection. Memory misses these and the contract silently breaks |
| Read this skill's SKILL.md text describing the template → write from that | The SKILL.md describes; the template file *is*. Read the file itself, write it verbatim |
## When ruff finds something Claude didn't write
A common case: Claude edits one function in a file that already had
unrelated `D`-rule violations. Ruff will report those too.
- **In scope of this turn**: the lines Claude touched. Fix those.
- **Out of scope**: pre-existing warnings in untouched code.
Mention them in the response so the user can choose to address
them, but don't drag every warning into the current task.
This keeps PR scope tight and avoids "while I was here" expansion
that the user didn't ask for.
## Companion skills
- **`data-science-python-stack`** — owns the decision that ruff is
Tier 1 mandatory; this skill assumes ruff is already installed.
If `pixi run ruff --version` fails, return there for the install.
- **`python-env-manager`** — turns "ruff is missing" into the right
install command for the project's manager. Don't run `pip install
ruff` in a pixi project.
- **`organize-ml-workspace`** — sets up the directory layout that
the bundled `ruff.toml`'s per-file ignores (`experiments/**` and
`audit/**`) reference. Drop the template *after* this skill has
run, so the paths it ignores actually exist.
- **`audit-ml-pipeline`** — generates audit files at
`audit/<NN>_<short_name>.py`. This skill's per-file ignores
cover the audit/ path the same way they cover experiments/.
- **`explore-ml-data`** — generates the EDA script `data/eda.py`
(same `# %%` cell convention). The bundled `ruff.toml`
per-file-ignores `data/eda.py` exactly like `audit/**`; lint it
with the same trio after writing it.
- **`update-config`** — only relevant if the user explicitly asks
for an automated lint hook later. Default is no hook.
## Conventions
- **Manual, not automatic.** Claude calls ruff itself, file by
file. No hook.
- **One-fix-per-file rule.** Hard cap on retries — second pass max,
then surface.
- **Project-root config only.** `ruff.toml` lives at the project
root and is the single source of truth. Don't add per-directory
overrides unless the user asks.
- **Don't widen scope on touched files.** Fix what Claude wrote;
surface (don't auto-fix) pre-existing issues elsewhere.More General & Other skills
find-skills
vercel-labs/skills
Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
grill-me
mattpocock/skills
A relentless interview to sharpen a plan or design.
grill-with-docs
mattpocock/skills
A relentless interview to sharpen a plan or design, which also creates docs (ADR's and glossary) as we go.

