python-package/skills/code-quality/SKILL.md
This skill should be used when the user is configuring Ruff, setting up mypy, adding type hints, choosing between mypy and pyright, writing py.typed markers, modernizing type annotations (PEP 695/649), using TYPE_CHECKING, setting up pre-commit hooks, configuring ruff format, choosing lint rule sets, or reviewing code quality tooling. Covers Ruff rule sets, mypy strict mode, pyright, modern typing patterns, pre-commit configuration, formatting, and complexity thresholds.
npx skillsauth add oborchers/fractional-cto code-qualityInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
The modern Python code quality stack has consolidated around two tools: Ruff for linting and formatting, mypy (or pyright) for type checking. Ruff replaces Black, isort, flake8, pyupgrade, autoflake, and all flake8 plugins with a single Rust-powered binary that is 10-100x faster. Every major package -- FastAPI, Pydantic, httpx, Polars, Rich, pytest, attrs -- has migrated to Ruff. There is no credible alternative for new projects.
Without this consolidation, teams juggle conflicting configurations across six or more tools, deal with parser incompatibilities, and waste CI minutes. Without type checking in CI, a py.typed marker becomes a lie -- promising type safety to downstream consumers while delivering none.
Start with an explicit select of rules you want. Never use select = ["ALL"] with a long ignore list -- every Ruff update adds new rules that fire unexpectedly.
[tool.ruff.lint]
select = [
"F", # Pyflakes -- undefined names, unused imports
"E", # pycodestyle errors
"W", # pycodestyle warnings
"I", # isort -- import sorting
"N", # pep8-naming -- naming conventions
"UP", # pyupgrade -- modernize syntax
"B", # flake8-bugbear -- common bugs
"SIM", # flake8-simplify -- simplifiable code
"C4", # flake8-comprehensions -- better comprehensions
"RUF", # Ruff-specific rules
"PERF", # Perflint -- performance anti-patterns
]
ignore = ["E501"] # Line too long -- handled by formatter
Also consider adding:
"TCH" -- moves type-only imports behind if TYPE_CHECKING: guards, reducing import time and breaking circular imports. Most valuable for typed libraries where downstream users pay your import cost."C90" -- flags functions exceeding a cyclomatic complexity threshold (default 10). A code review guardrail for functions that are too complex to maintain and test. Pair with [tool.ruff.lint.mccabe] max-complexity = 10.| Tier | Prefixes | Notes |
|------|----------|-------|
| Must have | F, E, W, I, UP, B | Every top package enables these. Zero noise. |
| Recommended | N, SIM, C4, RUF, PERF | Enabled by Pydantic, Polars, attrs. Catches real issues. |
| Consider | S, D, TCH, PT, C90, PLR | Useful with targeted ignores. TCH valuable for typed libraries. |
| Optional | A, FBT, ARG, ERA, ANN | Opinionated or high false-positive rate. Cherry-pick individual rules. |
Relax rules where they create noise rather than value:
[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S101", "D", "ANN", "PLR2004"]
"__init__.py" = ["F401"] # Allow unused imports (re-exports)
[tool.ruff]
target-version = "py310" # Match your requires-python lower bound
line-length = 88 # Black default, de facto standard
src = ["src"] # Critical for correct first-party import detection
[tool.ruff.lint.isort]
known-first-party = ["my_package"]
[tool.ruff.format]
docstring-code-format = true
Set src = ["src"] when using src layout. Without it, Ruff cannot distinguish first-party from third-party imports and isort grouping breaks silently.
Use ruff format as a drop-in Black replacement. Same defaults (line length 88, double quotes, magic trailing comma respected), but 30-100x faster.
| Setting | Keep Default | Change Only If |
|---------|-------------|----------------|
| quote-style = "double" | Yes | Team strongly prefers single quotes |
| line-length = 88 | Yes | Corporate standard requires different |
| skip-magic-trailing-comma = false | Yes | Never change -- preserves developer formatting intent |
| docstring-code-format = true | Enable this | No reason not to |
| Aspect | mypy | pyright |
|--------|------|---------|
| Use when | Need plugins (pydantic, django, sqlalchemy) | Want fastest checks, strictest analysis |
| Strictness | strict = true flag | typeCheckingMode = "strict" |
| Speed | Moderate (use daemon mode) | Consistently fast |
| IDE | External tool | Powers VS Code Pylance |
| Plugin support | Rich ecosystem | Limited |
For library authors: Run both. Different checkers catch different issues. Pydantic, FastAPI, and httpx do this.
[tool.mypy]
python_version = "3.10"
strict = true
warn_return_any = true
warn_unused_configs = true
enable_error_code = ["ignore-without-code", "redundant-cast", "truthy-bool"]
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
Always specify error codes in # type: ignore[error-code]. Bare # type: ignore silences all errors on the line, hiding real bugs. Ruff rule PGH003 catches this automatically.
[tool.pyright]
pythonVersion = "3.10"
typeCheckingMode = "standard"
reportUnnecessaryTypeIgnoreComment = true
enableTypeIgnoreComments = false
typeCheckingMode = "standard" is the practical starting point -- strict pyright is significantly noisier than strict mypy, and most exemplar libraries (Pydantic, Flask, SQLAlchemy) do not use strict mode. enableTypeIgnoreComments = false separates suppression concerns: # type: ignore[code] is for mypy only, # pyright: ignore[rule] is for pyright only. This is the pattern typeshed uses and eliminates cross-contamination between the two checkers.
Use the newest syntax your minimum Python version supports. Ruff's UP rules auto-fix old patterns.
| Old Pattern | Modern Pattern | Since |
|-------------|---------------|-------|
| List[str], Dict[str, Any] | list[str], dict[str, Any] | Python 3.9 |
| Optional[str], Union[str, int] | str \| None, str \| int | Python 3.10 |
| TypeVar("T") + Generic[T] | class Foo[T]: | Python 3.12 (PEP 695) |
| TypeAlias = Union[...] | type Alias = str \| int | Python 3.12 (PEP 695) |
| from __future__ import annotations | Remove it | Python 3.14 (PEP 649) |
py.typedEvery typed package must include an empty py.typed marker file and the Typing :: Typed classifier. Without it, type checkers ignore your annotations for downstream users. See the project-structure skill for the full explanation, file placement, and PEP 561 details.
Move type-only imports behind TYPE_CHECKING to avoid circular imports and reduce import time:
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from heavy_module import ExpensiveClass
Ruff's TCH rules detect imports that should be behind this guard.
Pydantic and runtime annotation frameworks: Do not guard imports behind TYPE_CHECKING if the type is used in Pydantic models, dataclasses with runtime validation, or any framework that evaluates annotations at runtime (serializers, dependency injection containers). Pydantic needs the actual class at runtime for validation -- guarding it behind TYPE_CHECKING causes NameError. When using TCH rules, configure Ruff to recognize your runtime annotation framework: [tool.ruff.lint.flake8-type-checking] runtime-evaluated-base-classes = ["pydantic.BaseModel"].
Collapse the entire pre-commit config to two Ruff hooks plus basic file hygiene. Do not add mypy, pytest, or security scanners -- they are too slow for pre-commit and belong in CI.
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-toml
- id: check-added-large-files
args: ["--maxkb=500"]
- id: check-merge-conflict
- id: debug-statements
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.7
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
Run ruff (linter with --fix) before ruff-format so auto-fixes are properly formatted.
Complete pyproject.toml quality tooling section:
# -- Ruff ---------------------------------------------------------
[tool.ruff]
target-version = "py310"
line-length = 88
src = ["src"]
[tool.ruff.lint]
select = ["F", "E", "W", "I", "N", "UP", "B", "SIM", "C4", "RUF", "PERF", "TCH", "C90"]
ignore = ["E501"]
[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S101", "PLR2004"]
"__init__.py" = ["F401"]
[tool.ruff.lint.isort]
known-first-party = ["my_package"]
[tool.ruff.lint.mccabe]
max-complexity = 10
[tool.ruff.format]
docstring-code-format = true
# -- mypy ----------------------------------------------------------
[tool.mypy]
python_version = "3.10"
strict = true
warn_return_any = true
warn_unused_configs = true
enable_error_code = ["ignore-without-code", "redundant-cast", "truthy-bool"]
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
# -- pyright (for libraries running both checkers) -----------------
[tool.pyright]
pythonVersion = "3.10"
typeCheckingMode = "standard"
reportUnnecessaryTypeIgnoreComment = true
enableTypeIgnoreComments = false
# -- Dependency group ----------------------------------------------
[dependency-groups]
lint = ["ruff>=0.9", "mypy>=1.14", "pyright>=1.1"]
When reviewing code for quality tooling and type safety:
tool.ruff.lint.select uses explicit opt-in, not select = ["ALL"] with a long ignore listsrc = ["src"] is set in [tool.ruff] for src-layout projectsE501 is in the ignore list when using ruff format (formatter handles line length)strict = true) with enable_error_code = ["ignore-without-code"]# type: ignore comments -- all have specific error codestypeCheckingMode = "standard", enableTypeIgnoreComments = false)py.typed marker file exists in the package root and is included in the wheellist[str], str | None)ruff hook runs before ruff-format in pre-commit orderingper-file-ignores relaxes rules for tests and __init__.py re-exportsmax-complexity = 10) if C90 is enabledTYPE_CHECKING guards do not hide types used by runtime validation frameworks (Pydantic, attrs, dataclasses)tools
This skill should be used when the user invokes any /plan-* command from the planning-tools plugin (/plan-context, /plan-master, /plan-open-questions, /plan-verify, /plan-tick, /plan-progress, /plan-delete), asks how Claude Code's plan files work, asks where plans are stored, asks to author or audit a multi-phase master planning document, asks how to walk through a plan's Open Questions interactively, asks how to write progress entries, or mentions ~/.claude/plans/ or .claude/planning-tools.local.md. Provides the index of planning-tools commands, the master-plan workflow lifecycle, the v0.3.0+ list-shape mandate (phases and questions as headings + bulleted scope items, never tables), the v0.3.2+ plain-bullet shape (no `- [ ]` checkboxes — heading emoji is the sole tick signal), the progress-entry methodology, and the mechanics of Claude Code's plan-mode file storage.
testing
This skill should be used by the plan-verifier agent and the /plan-verify command to audit a drafted master plan against a fixed checklist. Covers universal-core completeness, the v0.3.0+ no-tables-for-phases-or-questions rule, trigger-based section-coverage gaps, phase actionability (heading + per-phase TL;DR + bulleted scope + exit criteria), the v0.3.1+ per-phase TL;DR requirement, the v0.3.2+ plain-bullet scope shape (legacy `- [ ]`/`- [x]` accepted silently), the v0.3.3+ context-block shape (plan-level `**TL;DR:**` + bulleted metadata, legacy `>` blockquote accepted silently), integer phase numbering enforcement, dependency traceability, citation resolution, callout/evidence convention compliance, Open Questions placement, and the one-PR-per-master-plan rule. Single-owner of the audit checklist.
tools
This skill should be used when authoring, reviewing, or modifying a multi-phase master planning document via the planning-tools plugin (especially the /plan-master and /plan-verify commands). Codifies the universal core sections, trigger-based optional sections, integer-only phase numbering, Open Questions placement, one-PR-per-plan rule, status conventions, evidence attribution, callouts, cross-reference formats, the v0.3.0 list-shape mandate (phases and questions are heading + bulleted list, never markdown tables), the v0.3.1 per-phase TL;DR requirement (1–3 sentence what/why summary under each phase heading for glance-ability), the v0.3.2 plain-bullet scope shape (`- <action>` items, no `- [ ]` checkboxes — the phase status emoji is the sole tick signal), and the v0.3.3 context-block shape (a plan-level `**TL;DR:**` + a bulleted metadata list instead of a `>` blockquote; legacy blockquote blocks accepted silently). Project-agnostic — no ticket-prefix or plan-type taxonomy.
testing
This skill should be used when the user is adjusting spacing, padding, margins, content density, section gaps, vertical rhythm, or separation between elements. Also applies when reviewing whether a design feels cramped or too sparse, choosing between borders and whitespace for separation, or defining a spacing system. Covers the 4px/8px spacing system, macro vs micro whitespace, content density spectrum, separation techniques (whitespace > background shifts > borders), and vertical rhythm.