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.
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.
version
1.0.0
Use Ruff as the Single Linter and Formatter, Enforce Types with mypy
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.
Ruff Rule Sets
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.
"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.
Rule tier reference
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.
Per-file ignores
Relax rules where they create noise rather than value:
[tool.ruff]target-version = "py310"# Match your requires-python lower boundline-length = 88# Black default, de facto standardsrc = ["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.
Formatting with Ruff Format
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
Type Checking: mypy vs pyright
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.
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.
pyright configuration (for libraries running both checkers)
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.
Modern Type Hints
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)
Ship py.typed
Every 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.
TYPE_CHECKING guard
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"].
Pre-commit Hooks
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.