| name | ruff-strict-compliance |
| description | Enforces strict, zero-warning compliance with Ruff linting and formatting. Use when writing or editing Python in a Ruff-linted project, fixing `ruff check` or `ruff format` failures, or when tempted to add `# noqa` / dismiss a warning as "stylistic" or "not applicable to CLI tools like Typer." |
Ruff Strict Compliance
Overview
This skill enforces strict, zero-warning compliance with the Ruff linter and formatter. Coding agents often vibe-code around linter errors by dismissing them as "stylistic/optional" or lazy-patching them with # noqa comments. This skill bans those practices and mandates writing clean, idiomatic Python that naturally passes Ruff.
Core Mandates
1. No Excuses, No Dismissals
- You are never allowed to dismiss a Ruff warning as "purely stylistic," "optional," or "non-critical."
- You must fix the underlying code pattern. If a rule the project has enabled flags it, treat it as a code smell — note that several rule families used below (PTH, TRY, BLE, G, ERA, S) are opt-in strict families that only fire when the project enables them (see "Enabling these rules").
2. Zero-Tolerance for # noqa
- Do NOT add
# noqa or # type: ignore comments to bypass Ruff alerts.
- You may only use
# noqa if there is a documented, unavoidable library conflict (e.g., importing a module to trigger side effects in a legacy framework where there is no entrypoint). Even then, you must seek explicit user approval first.
3. Verification
- After editing any Python code, you must run
ruff check and ruff format to verify compliance. Do not wait for the user to report lint failures.
Enabling these rules
Ruff's default select only enables E4, E7, E9, and F. Several recipes below (PTH, TRY, BLE, G, ERA, S, plus B, C4, SIM, RET, UP, A, RUF) cover opt-in families that only fire if the project enables them in pyproject.toml:
[tool.ruff.lint]
select = ["ALL"]
ignore = ["D", "ANN", "COM812"]
If the project has no such config, propose adding one rather than assuming these rules are active.
Refactoring Recipes for Common Rules
1. Typer CLI (Resolving B008, F841, etc.)
The Problem: In Typer, developers often write CLI options using typer.Option(...) or typer.Argument(...) directly in function signatures as default values. This triggers Ruff rule B008 (Do not perform function call typer.Option in argument defaults).
To bypass this, lazy agents write # noqa: B008 or # noqa: F841.
The Right Way (Typer with Annotated):
Use Python's typing.Annotated (or typing_extensions.Annotated for Python < 3.9) to define options and arguments. This completely avoids B008 and is the modern, type-safe standard recommended by Typer.
import typer
app = typer.Typer()
@app.command()
def main(
name: str = typer.Option("World", help="Who to greet"),
verbose: bool = typer.Option(False, "--verbose", "-v")
):
print(f"Hello {name}")
import typer
from typing import Annotated
app = typer.Typer()
@app.command()
def main(
name: Annotated[str, typer.Option(help="Who to greet")] = "World",
verbose: Annotated[bool, typer.Option("--verbose", "-v")] = False
):
print(f"Hello {name}")
2. Mutable Default Arguments (B006 / B008)
The Problem: Using mutable structures like empty lists [] or dicts {} or function calls as default arguments.
def process_items(items: list = [], config: dict = dict()):
items.append("new_item")
return items
from typing import Optional
def process_items(items: Optional[list] = None, config: Optional[dict] = None):
if items is None:
items = []
if config is None:
config = {}
items.append("new_item")
return items
3. Unused Imports & Variables (F401, F841)
The Problem: Leaving debug imports or assigning variables that are never read.
import os
import sys
def compute(x):
result = x * 2
return x
def compute(x):
return x
4. Star Imports (F403 / F405)
The Problem: Importing everything from a module (from module import *), which pollutes the namespace and breaks static analysis.
from math import *
from math import pi, sin, cos
5. Blind Exceptions (BLE) & Tryceratops (TRY)
The Problem: Coding agents often catch broad, generic exceptions (Exception) to suppress errors, triggering rule BLE001 (Do not catch blind Exception). Furthermore, they use generic Exception types or embed verbose strings directly in raised errors, triggering Tryceratops (TRY) rules (e.g., TRY002 for custom exceptions, TRY003 for avoiding long exception messages).
def process_data(data):
try:
return data["value"] * 10
except Exception:
raise Exception("We failed to process the data because the value key was missing or malformed")
class DataProcessingError(Exception):
"""Custom exception for domain-specific errors."""
pass
def process_data(data: dict) -> int:
try:
return data["value"] * 10
except KeyError as e:
raise DataProcessingError("Missing 'value' key") from e
except TypeError as e:
raise DataProcessingError("Value is not a number") from e
6. Legacy os.path vs Modern pathlib (PTH)
The Problem: Using legacy string-based path manipulations (os.path.join, os.path.exists, os.path.abspath) instead of Python's modern object-oriented pathlib.Path. This is a very common legacy habit that Ruff flags under PTH rules.
import os
def get_config_content(filename):
full_path = os.path.join(os.getcwd(), "config", filename)
if os.path.exists(full_path):
with open(full_path, "r") as f:
return f.read()
return ""
from pathlib import Path
def get_config_content(filename: str) -> str:
full_path = Path.cwd() / "config" / filename
if full_path.exists():
return full_path.read_text(encoding="utf-8")
return ""
7. Mutable Class Attributes (RUF012)
The Problem: Declaring a mutable collection (like a list or dictionary) as a class attribute without explicitly annotating it as a ClassVar or defining it inside __init__. In standard classes and dataclasses, this results in shared state among all instances, causing subtle bugs. Ruff flags this under RUF012.
class ProjectManager:
active_tasks: list[str] = []
default_config: dict[str, str] = {}
from typing import ClassVar
class ProjectManager:
active_tasks: ClassVar[list[str]] = []
default_config: ClassVar[dict[str, str]] = {}
class ProjectManager:
def __init__(self) -> None:
self.active_tasks: list[str] = []
self.default_config: dict[str, str] = {}
8. Unnecessary Comprehensions & Functional Wrappers (C4)
The Problem: Writing unnecessarily complex or redundant structures like list([x for x in data]) or functional wrappers instead of clean native syntax. Ruff flags these under C4 (flake8-comprehensions) rules.
def get_names(users):
names_list = list([u.name for u in users])
empty_dict = dict()
return names_list
def get_names(users) -> list[str]:
names_list = [u.name for u in users]
empty_dict = {}
return names_list
9. Unnecessary Elif/Else and Returns (RET)
The Problem: Storing a return value in a temporary variable only to return it on the next line (RET504), or using else / elif branches after a preceding block has already returned or raised (RET505 / RET506).
def check_value(x):
if x > 10:
result = "large"
return result
else:
return "small"
def check_value(x) -> str:
if x > 10:
return "large"
return "small"
10. Simplify Rules (SIM)
The Problem: Writing verbose nested if statements or manually returning True/False based on boolean evaluations, which violates SIM (flake8-simplify) rules.
def is_valid_user(user):
if user.is_active:
if user.has_permission:
return True
return False
def is_valid_user(user) -> bool:
return bool(user.is_active and user.has_permission)
11. Legacy Type Annotations & Syntax (UP)
The Problem: Using deprecated structures or importing typing wrappers like List, Dict, Tuple, or Set under Python 3.9+ instead of using lowercase builtins (list, dict, tuple, set). These are flagged under UP (pyupgrade) rules.
from typing import List, Dict, Tuple
def process_data(items: List[str]) -> Dict[str, Tuple[int, int]]:
pass
def process_data(items: list[str]) -> dict[str, tuple[int, int]]:
pass
12. Builtin Shadowing (A)
The Problem: Naming variables, function arguments, or class attributes after builtins (like id, type, list, dict, dir, input, min, max). This is flagged under A001 / A002 / A003.
def get_user_by_id(id: int):
list = ["active", "inactive"]
return list
def get_user_by_id(user_id: int) -> list[str]:
statuses = ["active", "inactive"]
return statuses
13. Logging Format and F-Strings (G)
The Problem: Using f-strings, .format(), or string concatenation inside logging statements (e.g. logger.info(f"User {name} logged in")). Doing this forces string interpolation immediately, even if the log level is disabled. Ruff flags this under G004 (logging-f-string).
import logging
logger = logging.getLogger(__name__)
def log_event(name):
logger.info(f"Processing event: {name}")
def log_event(name: str):
logger.info("Processing event: %s", name)
14. Insecure Assertions & Silently Swallowed Exceptions (S)
The Problem: Using assert statements to validate program inputs or critical runtime flow in production code (S101). When Python is run in optimized mode (-O), all assert statements are stripped out, rendering validation useless. Also, an exception handler whose body is just pass is flagged under S110 (try-except-pass), and one that is just continue under S112 (try-except-continue) — these fire on the bare pass/continue handler itself, regardless of anything else; resolve them by actually handling, logging, or re-raising the exception.
def process_age(age: int):
assert age >= 0, "Age cannot be negative"
try:
do_something()
except ValueError:
pass
def process_age(age: int):
if age < 0:
raise ValueError("Age cannot be negative")
try:
do_something()
except ValueError as e:
logger.warning("Swallowing expected error: %s", e)
15. Commented-Out Code (ERA)
The Problem: Leaving commented-out chunks of code in files, which clutters files. This is flagged under ERA001.
def calculate(x):
return x * 2
def calculate(x: int) -> int:
return x * 2
Checklist Before Ending Your Turn
- Format: Run
ruff format . to make sure all code matches the project styling.
- Lint: Run
ruff check . to inspect all warnings.
- Fix: Refactor any violations using the clean patterns described above. Do NOT use
# noqa.