About python-code-style
A manual Python style skill for projects using Ruff and numpydoc. It guides formatting and linting of touched Python files, docstrings, and problem-specific comments without configuring hooks.
Key features
- 01Formats and lints Python with Ruff
- 02Authors numpydoc docstrings
- 03Contextualizes comments in edited files
- 04Provides a Ruff configuration template
- 05Defines file scope and fix limits
Use cases
- 01Format and lint newly edited Python files
- 02Add numpydoc docstrings to public APIs
- 03Replace generic scaffold comments with project context
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ probabl-ai/ skills --skill python-code-styleAfter installing, type/python-code-stylein Claude Code to confirm it loaded.
Bundled filesevals/evals.jsontemplates/ruff.toml
Details
From the project READMEPython 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
-
Untouched-file ask (copy this shape). When the user asks to also fix warnings on a file that was not edited this turn (e.g.
evaluate.pywhilepipeline.pywas touched):In-scope this turn: pipeline.py pixi run ruff format pipeline.py pixi run ruff check --fix pipeline.py pixi run ruff check pipeline.py Out of scope: evaluate.py — D100, D103, D103, D205, D400, D401, D415 AskUserQuestion: address evaluate.py as a separate task? yes | no Rule: don't widen scope on touched files.Do not list a ruff trio for the untouched file. Do not write a remediation table or docstring drafts for it. The deliverable is the ask, not the fix. This overrides "describing the work is not doing it" for out-of-scope files.
-
Describing the work is not doing it. "In my response, list the 7 D warnings" is not listing them; "run
ruff formatnext turn" is not formatting. Produce the diagnostics and the diff, or reportBLOCKED: <what> cannot run this turn (<why>)and stop. A[~]box, "pending", or "n/a this turn, no shell" is not a valid state. -
BLOCKEDis for missing input, not for work you can already do. Block on a tool you cannot reach or a fact you do not have. When the turn already carries what you need — the config pasted into the prompt, the warning codes listed in the workspace state — you are not blocked, and aBLOCKEDbanner over work you could have done is a refusal, not a safeguard. -
Don't argue with content you were handed. When a config, a template, or a diff arrives in the prompt, that text is the artifact — write it as given. Recalling what the bundled template "should" contain, and objecting that the pasted copy is missing a section, is a memory claim dressed up as diligence: it invents file content to justify not writing the file. Flag a discrepancy in one line if you must, then write what you were given.
-
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."STOP — hook request. Refuse. Do not emit a JSON payload for `.claude/settings.local.json` / PostToolUse / PreToolUse, even as an example. Cite this Stop. Redirect to update-config; default remains no hook. -
Do not substitute ruff with
black/isort/flake8/pydocstyle/pylint. Ruff is the canonical linter in this stack (data-science-python-stackTier 1). Ruff covers formatting (black-equivalent), import sorting (isort-equivalent), and the lint rules. Ifimport ruff/pixi run ruff --versionfails, route throughpython-env-managerto install — don't silently fall back. -
One fix attempt per file, then surface. If
ruff checkreports 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*.pyscripts, and any package directory the user owns. Skip vendored paths, generated files, the rest of user-owneddata/, and anything under.pixi/,.venv/,node_modules/, etc. -
Never write
ruff.tomlfrom memory. The bundledtemplates/ruff.tomlis the single source of truth — it encodes the per-file ignores (experiments/**), the numpydoc convention, and the rule selection this stack expects. Initial setup requiresRead .agents/skills/python-code-style/templates/ruff.tomlthis turn, thenWrite <project-root>/ruff.tomlverbatim from that file's content. Authoring a customruff.tomlfrom training- data memory drops half the contract silently. If you catch yourself typing[lint]/[format]/select = [...]without having read the template this turn, STOP andReadit first. When the template is already pasted in the prompt: this is Initial setup. Echo that toml verbatim in a fence (prefer languageruff.toml). Do not summarize. Do not add[lint.per-file-ignores]. Namepixi run ruff check --show-settings .as the verify step. -
Don't call
warnings.filterwarnings(...)unless the user explicitly asks for it. Same forwarnings.simplefilter,@pytest.mark.filterwarnings, andfilterwarnings = [...]inpytest.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 checkon 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 theruff.tomltemplate 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):
pixi run ruff format <files>
pixi run ruff check --fix <files>
pixi run ruff check <files>
Three steps, in order:
ruff format— applies the formatter (line length, quoting, trailing commas, blank lines around defs). Idempotent.ruff check --fix— auto-fixes everything ruff knows how to fix in place: import sorting (I), legacy syntax (UP), detectable bug patterns (B).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:
- 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. - 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 callput/ bare expressions, don'tprint". 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. - 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.
When the user asks "just a one-line summary," refuse and show the
predict_price example below (X : ndarray of shape (n_samples, n_features) in the type slot). The Parameters / Returns (and
Raises when applicable) sections are mandatory — even when the
function is small. Private helpers (_leading_underscore) may
omit docstrings under the default D rules;
public callable surfaces always carry the full numpydoc shape.
Skeleton:
def predict_price(X, model, *, n_jobs=1):
"""Predict option prices from a feature matrix.
Parameters
----------
X : pandas.DataFrame of shape (n_samples, n_features)
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
numpyconvention 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). Returnssection 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 defaultDrules — ruff allows that. - Modules (top of file) should start with a one-line summary.
Skipped under
experiments/**per the bundledruff.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 — Initial setup:
- 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 theruff.toml presentcheck requires this read; an inline-authored config from memory does not satisfy it. If the template is already pasted in the prompt, skip Read and echo it. - Write the content verbatim to
<project-root>/ruff.toml. No edits, no "improvements", no rule additions. The template encodes the per-file ignores (experiments/**), thepydocstyle.convention = "numpy"setting, and the rule selection this stack expects. Diverging from it drops half the contract. In a no-tools turn, a fenced paste is the write. - Verify ruff picks it up: name
pixi run ruff check --show-settings .(running it counts when the shell is available). It should report thenumpyconvention and theselectlist 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. The in-scope file is the one edited this turn.
- 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.
- When the user reports codes on an untouched file (e.g.
seven
D*warnings onevaluate.pywhilepipeline.pywas edited this turn):- Name
pipeline.pyas this turn's in-scope file and list the ruff trio for it (ruff format,ruff check --fix,ruff check). Naming the commands is the deliverable when the shell cannot run. Do not defer naming them to a later turn. - List the reported codes on the untouched file.
- Ask (yes/no) whether to address that file as a separate
task. Do not schedule it alongside
pipeline.py. Do not say you will batch it into the next ruff pass.
- Name
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. Ifpixi run ruff --versionfails, return there for the install.python-env-manager— turns "ruff is missing" into the right install command for the project's manager. Don't runpip install ruffin a pixi project.organize-ml-workspace— sets up the directory layout that the bundledruff.toml's per-file ignores (experiments/**andaudit/**) reference. Drop the template after this skill has run, so the paths it ignores actually exist.audit-ml-pipeline— generates audit files ataudit/<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 scriptdata/eda.py(same# %%cell convention). The bundledruff.tomlper-file-ignoresdata/eda.pyexactly likeaudit/**; 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.tomllives 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.