The config was already perfect. [tool.ruff] sat in pyproject.toml with a sensible rule selection: E, F, B, I, TCH, SIM, UP, YTT. The only problem was that ruff had never been installed, never run, and never enforced. The first honest invocation reported 374 errors. This post is the playbook for turning that number into a green gate without a monster diff or a gutted rule set.
Step one: let the machine do the boring part
167 of the violations were safe autofixes: import sorting (53), PEP 585 and 604 annotation modernization (62), unused imports. One command cleared them. Two lessons from that pass are worth the price of admission:
- Autofixers do not understand re-export intent. The fixer stripped
from pydantic.dataclasses import dataclass as modelout of core decorators because nothing in that module referenced it. It was the public re-export surface. Restored with an explicit__all__, which both silences F401 and documents the API. - The linter polices its operator. A blanket rename I applied for B007 hit two loops; one used the variable. F821 caught my mistake before CI did. That is the tool earning trust in its first hour.
Step two: hand-fix what teaches something
Thirty-two violations remained after autofix, and every category repaid attention:
- B904, three raise-without-from sites inside Procedure validation. Adding
from vechains validation errors properly instead of silently swallowing context. - B023, loop-variable capture in the dev console’s command fallback lambda. Bound as a default argument (
lambda _, c=cmd), the classic closure fix. - SIM115 open-without-context in tests became with-blocks; one intentional temp-file case got an inline noqa with a written reason: uvicorn reload needs the path to outlive the write.
Step three: encode accepted style as documented config
200 of the original findings were line length and single-line compound statements, concentrated in CLI code where if x: raise typer.Exit(1) is house style. Mass-rewriting 200 lines would produce a noisy diff that reviews worse than it reads. Instead:
ignore = ["E501", "E701", "E702", "SIM108", "SIM117"]
With comments explaining each choice. The distinction matters: an ignore list nobody can explain is rot, but a deliberate, commented acceptance of current style is exactly how enforcement should start. Tighten later if a formatter lands.
Step four: make it someone else’s problem forever
A ruff job in CI now runs the exact local command. Green means the next 374 cannot accumulate quietly. Combined with Dependabot keeping the tool itself fresh, the standard finally exists in the only place standards are real: the merge path.
Final tally: zero violations, 176 tests passing, and three small bugs caught on day one. The config barely changed. The behavior changed completely.

pyRPC