
Linting
- 74 installs
- 7 repo stars
- Updated January 25, 2026
- jiatastic/open-python-skills
Python linting with Ruff: a fast Rust linter that replaces flake8, isort, pyupgrade, and autoflake, with auto-fix and CI integration.
About
Configures and runs Ruff for Python code quality, covering rule selection, auto-fix, suppressions, and CI usage. A developer uses it to standardize style, fix warnings, enforce rules in CI, or replace legacy linters like flake8 and isort.
- Single fast tool replacing flake8/isort/pyupgrade/autoflake with 800+ rules
- Start with E/F rules, safe --fix, and pyproject.toml as single source of truth
Linting by the numbers
- 74 all-time installs (skills.sh)
- +4 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #509 of 1,352 Code Review & Quality skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/jiatastic/open-python-skills --skill lintingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 74 |
|---|---|
| repo stars | ★ 7 |
| Last updated | January 25, 2026 |
| Repository | jiatastic/open-python-skills ↗ |
What it does
Python linting with Ruff: a fast Rust linter that replaces flake8, isort, pyupgrade, and autoflake, with auto-fix and CI integration.
Files
Ruff Linting
Ruff is an extremely fast Python linter designed as a drop-in replacement for Flake8 (plus dozens of plugins), isort, pydocstyle, pyupgrade, autoflake, and more. Written in Rust, it offers 10-100x performance improvements over traditional Python linters.
Overview
Ruff provides a single CLI for linting with optional auto-fix. It supports an extensive rule set with 800+ built-in rules and integrates cleanly with pre-commit, CI systems, and modern editors.
Key Features
- Extremely Fast: 10-100x faster than Flake8, Black, isort
- Drop-in Replacement: Compatible with existing Flake8 plugins and configurations
- Auto-fix Support: Automatically fix many common issues
- Comprehensive Rules: 800+ built-in rules from popular linters
- Single Tool: Replaces flake8, isort, pyupgrade, autoflake, pydocstyle, and more
When to Use
- Standardizing code quality across a project or team
- Enforcing consistent coding rules in CI/CD pipelines
- Replacing multiple linting tools with a single fast solution
- Auto-fixing common code style issues
- Migrating from Flake8, isort, or other legacy linters
Quick Start
# Install Ruff
uv pip install ruff
# or
pip install ruff
# Run linting on current directory
ruff check .
# Run linting with auto-fix
ruff check . --fix
# Watch mode for development
ruff check --watchCore Patterns
1. Start minimal: Enable E and F rules first, then gradually expand 2. Auto-fix safely: Use ruff check --fix for safe fixes only 3. Per-file ignores: Use sparingly for generated code or special cases 4. CI integration: Use ruff check --output-format github for GitHub Actions 5. Single source of truth: Configure via pyproject.toml or ruff.toml
Rule Selection
Ruff uses a code system where each rule consists of a 1-3 letter prefix followed by digits (e.g., F401). Rules are controlled via lint.select, lint.extend-select, and lint.ignore.
Recommended Rule Sets
Minimal (Start Here):
[tool.ruff.lint]
select = ["E", "F"] # pycodestyle errors + PyflakesBalanced (Recommended):
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"F", # Pyflakes
"UP", # pyupgrade
"B", # flake8-bugbear
"SIM", # flake8-simplify
"I", # isort
]Comprehensive:
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # Pyflakes
"UP", # pyupgrade
"B", # flake8-bugbear
"SIM", # flake8-simplify
"I", # isort
"N", # pep8-naming
"S", # flake8-bandit (security)
"C4", # flake8-comprehensions
"DTZ", # flake8-datetimez
"T20", # flake8-print
"RUF", # Ruff-specific rules
]
ignore = ["E501"] # Line too long (handled by formatter)Rule Priority
CLI options override pyproject.toml, which overrides inherited configs: 1. CLI (--select, --ignore) - highest priority 2. Current pyproject.toml 3. Inherited pyproject.toml files
For detailed rule configuration, see references/rule_selection.md.
Configuration
pyproject.toml (Recommended)
[tool.ruff]
line-length = 88
target-version = "py311"
exclude = [".venv", "dist", "build", "*.pyi"]
[tool.ruff.lint]
select = ["E", "F", "UP", "B", "SIM", "I"]
ignore = ["E501"]
fixable = ["ALL"]
unfixable = []
[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S101"] # Allow assert in tests
"__init__.py" = ["F401"] # Allow unused imports
"**/{tests,docs,tools}/*" = ["E402"] # Allow late imports
[tool.ruff.lint.isort]
known-first-party = ["myproject"]
[tool.ruff.lint.pydocstyle]
convention = "google"ruff.toml Alternative
line-length = 88
target-version = "py311"
[lint]
select = ["E", "F", "UP", "B", "SIM", "I"]
ignore = ["E501"]
[lint.per-file-ignores]
"tests/**/*.py" = ["S101"]Fix Safety
Ruff categorizes fixes as safe or unsafe:
| Type | Behavior | Default |
|---|---|---|
| Safe | Preserves code semantics | Enabled |
| Unsafe | May change runtime behavior | Disabled |
# Apply only safe fixes (default)
ruff check --fix
# Apply all fixes including unsafe
ruff check --fix --unsafe-fixes
# Show what unsafe fixes are available
ruff check --unsafe-fixesAdjusting Fix Safety
[tool.ruff.lint]
# Promote unsafe fixes to safe
extend-safe-fixes = ["F601"]
# Demote safe fixes to unsafe
extend-unsafe-fixes = ["UP034"]
# Control which rules can be fixed
fixable = ["ALL"]
unfixable = ["F401"] # Never auto-fix unused importsFor detailed fix safety documentation, see references/fix_safety.md.
Error Suppression
Line-Level (noqa)
x = 1 # noqa: F841 # Ignore specific rule
i = 1 # noqa: E741, F841 # Ignore multiple rules
x = 1 # noqa # Ignore all rules (avoid this)File-Level
# ruff: noqa # Ignore all rules in file
# ruff: noqa: F841 # Ignore specific rule in fileBlock-Level (Preview Mode)
# ruff: disable[E501]
VALUE_1 = "Very long string..."
VALUE_2 = "Another long string..."
# ruff: enable[E501]For detailed suppression patterns, see references/error_suppression.md.
CLI Commands
# Basic linting
ruff check . # Lint current directory
ruff check path/to/file.py # Lint specific file
ruff check . --fix # Lint and auto-fix
ruff check . --fix --unsafe-fixes # Include unsafe fixes
# Output formats
ruff check . --output-format text # Default human-readable
ruff check . --output-format github # GitHub Actions annotations
ruff check . --output-format json # JSON output
ruff check . --output-format sarif # SARIF format
# Inspection
ruff check . --diff # Show what would change
ruff check . --show-fixes # Show available fixes
ruff check . --statistics # Show rule statistics
ruff rule F401 # Explain a specific rule
# Development
ruff check --watch # Watch mode
ruff check . --add-noqa # Add noqa comments
ruff check . --extend-select RUF100 # Find unused noqa commentsExit Codes
| Code | Meaning |
|---|---|
| 0 | No violations found, or all fixed |
| 1 | Violations found |
| 2 | Configuration error or internal error |
Modify exit behavior:
ruff check . --exit-zero # Always exit 0
ruff check . --exit-non-zero-on-fix # Exit 1 if any violations (even if fixed)CI Integration
GitHub Actions
name: Lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/ruff-action@v3
with:
args: "check --output-format github"Pre-commit
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-formatTroubleshooting
| Issue | Solution |
|---|---|
| Rule conflicts with formatter | Ignore formatting rules (E501) when using ruff format |
| Too many violations | Start with minimal rules (E, F), expand gradually |
| Too many per-file ignores | Review rule selection, consider disabling noisy rules |
| Slow on large codebase | Ensure .venv excluded, check for recursive symlinks |
| noqa not working | Check syntax: # noqa: F401 (colon required) |
Common Rule Prefixes
| Prefix | Source | Description |
|---|---|---|
| E/W | pycodestyle | Style errors/warnings |
| F | Pyflakes | Logical errors |
| B | flake8-bugbear | Common bugs |
| I | isort | Import sorting |
| UP | pyupgrade | Python version upgrades |
| SIM | flake8-simplify | Code simplification |
| N | pep8-naming | Naming conventions |
| S | flake8-bandit | Security issues |
| C4 | flake8-comprehensions | Comprehension style |
| RUF | Ruff | Ruff-specific rules |
References
- Quickstart Guide
- Rule Selection Reference
- Fix Safety Guide
- Error Suppression Patterns
- Common Pitfalls
- Official Documentation
- Full Rules Reference
Ruff Error Suppression Patterns
Comprehensive guide to suppressing lint errors in Ruff.
Overview
Ruff supports multiple mechanisms for suppressing lint errors: 1. Configuration-based: Global or per-file ignores in config files 2. Comment-based: Inline, file-level, and block-level suppressions
Configuration-Based Suppression
Global Ignores
Ignore rules across the entire project:
[tool.ruff.lint]
ignore = [
"E501", # Line too long
"W503", # Line break before binary operator
]Per-File Ignores
Ignore rules for specific files or directories:
[tool.ruff.lint.per-file-ignores]
# Single file
"src/generated/parser.py" = ["E501", "F401"]
# All files matching pattern
"__init__.py" = ["F401"]
# All files in directory (recursive)
"tests/**/*.py" = ["S101", "PLR2004"]
# Multiple directories
"**/{tests,docs,tools}/*" = ["E402"]
# By file suffix
"*_test.py" = ["S101"]
"test_*.py" = ["S101"]Pattern Examples
| Pattern | Matches |
|---|---|
"file.py" | Any file named file.py |
"path/to/file.py" | Specific file at that path |
"*.py" | All Python files |
"tests/*.py" | Python files directly in tests/ |
"tests/**/*.py" | All Python files in tests/ (recursive) |
"**/__init__.py" | All __init__.py files anywhere |
Comment-Based Suppression
Line-Level Suppression (noqa)
Suppress violations on a single line:
# Suppress specific rule
x = 1 # noqa: F841
# Suppress multiple rules
i = 1 # noqa: E741, F841
# Suppress all rules (avoid this pattern)
x = 1 # noqanoqa Syntax Rules
1. Case insensitive: noqa, NOQA, NoQa all work 2. Colon required for codes: # noqa: F401 (not # noqa F401) 3. Comma-separated: # noqa: F401, E501 4. Whitespace optional: #noqa:F401 works but # noqa: F401 is clearer
Correct vs Incorrect Examples
# ✅ Correct
x = 1 # noqa: F841
x = 1 # noqa: F841, E501
x = 1 #noqa: F841 # Works but less readable
# ❌ Incorrect
x = 1 # noqa F841 # Missing colon
x = 1 # noqa:F841 # Works, but add space after colon for readabilityMulti-line Strings
For docstrings and multi-line strings, place noqa at the end:
"""This is a very long docstring that exceeds the line length limit
and needs to be suppressed because it contains important information.
""" # noqa: E501Import Blocks
For isort suppressions, place noqa on the first import:
import os # noqa: I001
import abc
# The noqa applies to the entire import blockFile-Level Suppression
Suppress violations across an entire file:
# At the top of the file
# Suppress all violations
# ruff: noqa
# Suppress specific rule
# ruff: noqa: F841
# Suppress multiple rules
# ruff: noqa: F841, E501Placement
File-level noqa comments must be on their own line (not inline):
# ✅ Correct - own line
# ruff: noqa: F401
# ❌ Incorrect - inline
import os # ruff: noqa # This is line-level, not file-levelFlake8 Compatibility
Ruff also respects Flake8's file-level suppression:
# flake8: noqa
# flake8: noqa: F401Block-Level Suppression (Preview Mode)
Suppress violations within a range of code using disable/enable comments.
Note: This feature is only available in preview mode.
Basic Usage
# ruff: disable[E501]
VALUE_1 = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod"
VALUE_2 = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod"
VALUE_3 = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod"
# ruff: enable[E501]Multiple Rules
# ruff: disable[E741, F841]
i = 1
l = 2 # noqa: E741 would also work
O = 3
# ruff: enable[E741, F841]Syntax Rules
1. Case sensitive: Must use disable and enable exactly 2. Square brackets: [E501] not (E501) or : E501 3. Matching codes: Enable must have same codes as disable 4. Same indentation: Must be at same indentation level
Implicit Range End
If no matching enable comment is found, the range ends at the next less-indented scope:
def foo():
# ruff: disable[E741, F841]
i = 1
if True:
O = 1
l = 1
# implicit end of range
foo() # Not suppressedWarning: A RUF104 diagnostic is produced for implicit ranges. Use explicit enable comments.
What Doesn't Work
# ❌ Wrong - blanket suppression not supported
# ruff: disable
# ❌ Wrong - mismatched codes
# ruff: disable[E501]
# ruff: enable[E741] # Doesn't match
# ❌ Wrong - different order
# ruff: disable[E501, F841]
# ruff: enable[F841, E501] # Order must matchisort Action Comments
Ruff respects isort's action comments for import sorting:
Skip Entire File
# isort: skip_file
import sys
import os
import abcSkip Single Import
import os
import sys # isort: skip
import abcEnable/Disable Regions
import os
# isort: off
import module_b # Must be before module_a
import module_a
# isort: on
import abcForce Split
import os
# isort: split
import sys # New import group after splitRuff-Prefixed Variants
These variants work identically and are clearer:
# ruff: isort: skip_file
# ruff: isort: on
# ruff: isort: off
# ruff: isort: skip
# ruff: isort: splitDetecting Unused Suppressions
Ruff includes RUF100 to detect unused noqa comments:
# Find unused noqa comments
ruff check . --extend-select RUF100
# Remove unused noqa comments
ruff check . --extend-select RUF100 --fixExample:
# Before: noqa is unused (violation doesn't exist)
x = 1 # noqa: E501 # Line isn't actually too long
# After --fix:
x = 1Adding noqa Comments Automatically
Ruff can automatically add noqa comments to all violations:
ruff check . --add-noqaThis adds appropriate noqa comments to all lines with violations:
# Before
x = 1 # Unused variable
# After --add-noqa
x = 1 # noqa: F841Caution: This is useful for migrations but can hide real issues. Review manually.
Best Practices
1. Prefer Configuration Over Comments
# Instead of many noqa comments
[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S101"]2. Be Specific with Codes
# ✅ Good - specific
x = 1 # noqa: F841
# ❌ Avoid - blanket suppression
x = 1 # noqa3. Add Explanations
# ✅ Good - explains why
x = 1 # noqa: F841 - Required for side effect
# ❌ Less helpful
x = 1 # noqa: F8414. Minimize Suppressions
Review if you have many noqa comments for the same rule - consider:
- Adjusting the rule configuration
- Adding to per-file ignores
- Disabling the rule if it's too noisy
5. Audit Regularly
# Count noqa usage
grep -r "noqa" src/ | wc -l
# Find unused noqa
ruff check . --extend-select RUF1006. Document Team Decisions
[tool.ruff.lint]
ignore = [
# E501: We use a formatter that handles line length
"E501",
# S101: We use assert extensively in tests
"S101",
]Suppression Priority
When multiple suppression methods apply:
1. Line-level noqa (highest priority) 2. Block-level disable/enable 3. File-level noqa 4. Per-file ignores in config 5. Global ignore in config (lowest priority)
Example:
# ruff: noqa: F841 # File-level suppression
def foo():
x = 1 # F841 is suppressed by file-level comment
y = 2 # noqa: F841 # Would also suppress (line-level)Troubleshooting
noqa Not Working
1. Check syntax:
# ✅ Correct
x = 1 # noqa: F841
# ❌ Missing colon
x = 1 # noqa F8412. Check rule code:
ruff rule F841 # Verify rule code exists3. Check file is being linted:
ruff check path/to/file.pyPer-file Ignore Not Working
1. Check glob pattern:
# For recursive matching, use **
"tests/**/*.py" = ["S101"] # ✅
"tests/*.py" = ["S101"] # Only direct children2. Check file is included:
ruff check path/to/file.py --show-settingsBlock Suppression Not Working
1. Ensure preview mode is enabled:
[tool.ruff]
preview = true2. Check matching codes and indentation
Too Many Violations After Migration
1. Start with global ignore, then refine:
[tool.ruff.lint]
ignore = ["E501"] # Temporarily ignore all2. Add noqa comments for remaining issues:
ruff check . --add-noqa3. Review and reduce suppressions over time
Ruff Fix Safety Guide
Comprehensive documentation on Ruff's safe and unsafe fix system.
Understanding Fix Safety
Ruff categorizes automatic fixes into two categories:
| Category | Description | Default Behavior |
|---|---|---|
| Safe | Preserves code semantics and runtime behavior | Enabled with --fix |
| Unsafe | May change runtime behavior or remove comments | Requires --unsafe-fixes |
Safe Fixes
Safe fixes guarantee that: 1. The meaning of your code is preserved 2. Runtime behavior remains unchanged 3. Comments are only removed when deleting entire statements/expressions
Examples of Safe Fixes
# Before: Unused variable (F841)
x = 1 # This variable is never used
y = 2
print(y)
# After safe fix: Removes the unused assignment
y = 2
print(y)# Before: Unnecessary pass (PIE790)
def foo():
pass
return 1
# After safe fix:
def foo():
return 1# Before: Duplicate key (F601)
d = {"a": 1, "a": 2}
# After safe fix:
d = {"a": 2}Unsafe Fixes
Unsafe fixes may: 1. Change runtime behavior 2. Change the type of exceptions raised 3. Remove comments 4. Alter control flow in edge cases
Examples of Unsafe Fixes
RUF015: Unnecessary Iterable Allocation
# Before
head = list(range(99999999))[0]
# After unsafe fix
head = next(iter(range(99999999)))Why unsafe? Changes exception from IndexError to StopIteration when collection is empty:
list(range(0))[0] # Raises IndexError
next(iter(range(0))) # Raises StopIterationUP038: Use isinstance() union type
# Before
isinstance(x, (int, float))
# After unsafe fix (Python 3.10+)
isinstance(x, int | float)Why unsafe? May fail at runtime on Python < 3.10.
Applying Fixes
Safe Fixes Only (Default)
ruff check . --fixOnly applies fixes that are guaranteed to preserve code semantics.
Preview Changes Before Applying
ruff check . --diffShows what would change without making modifications.
Include Unsafe Fixes
# Show unsafe fixes without applying
ruff check . --unsafe-fixes
# Apply all fixes including unsafe
ruff check . --fix --unsafe-fixesDisable Unsafe Fix Hints
By default, Ruff shows hints when unsafe fixes are available. To silence:
ruff check . --no-unsafe-fixesOr in configuration:
[tool.ruff]
unsafe-fixes = falseConfiguring Fix Safety
Promoting Unsafe Fixes to Safe
If you trust certain unsafe fixes, promote them to safe:
[tool.ruff.lint]
extend-safe-fixes = [
"F601", # Duplicate keys
"RUF015", # Unnecessary iterable allocation
"UP038", # isinstance union type
]You can use prefixes to promote entire categories:
[tool.ruff.lint]
extend-safe-fixes = ["F"] # All Pyflakes fixes become safeDemoting Safe Fixes to Unsafe
For extra caution, demote safe fixes:
[tool.ruff.lint]
extend-unsafe-fixes = [
"UP034", # Extraneous parentheses
"SIM", # All simplify rules
]Preventing Fixes Entirely
Use unfixable to completely disable fixes for certain rules:
[tool.ruff.lint]
unfixable = [
"F401", # Never auto-remove unused imports
"F841", # Never auto-remove unused variables
"B", # Never auto-fix any bugbear rules
]Allowing Only Specific Fixes
Use fixable to whitelist which rules can be fixed:
[tool.ruff.lint]
# Only these rules can be auto-fixed
fixable = ["I", "UP", "F401"]Or fix everything except specific rules:
[tool.ruff.lint]
fixable = ["ALL"]
unfixable = ["F401", "F841"]Fix Safety by Rule Category
Generally Safe to Auto-Fix
| Rule | Description |
|---|---|
| I (isort) | Import sorting |
| UP (pyupgrade) | Most Python upgrades |
| F401 | Unused imports |
| W (pycodestyle warnings) | Whitespace issues |
| SIM1XX | Simple simplifications |
Proceed with Caution
| Rule | Description | Why Careful? |
|---|---|---|
| B | flake8-bugbear | May change error handling |
| RUF015 | Iterable allocation | Changes exception types |
| F841 | Unused variables | May remove intentional assignments |
| TCH | Type checking | May break runtime type checks |
Recommended Configuration for Teams
[tool.ruff.lint]
# Enable all auto-fixes
fixable = ["ALL"]
# Prevent accidental removal of important code
unfixable = [
"F401", # Unused imports - may be intentional re-exports
"F841", # Unused variables - may be intentional
"ERA", # Commented code - may be needed
]
# Be extra careful with these
extend-unsafe-fixes = [
"B", # Bugbear changes may affect error handling
]Workflow Recommendations
Individual Development
# 1. Run with safe fixes
ruff check . --fix
# 2. Review remaining issues
ruff check .
# 3. Preview unsafe fixes
ruff check . --diff --unsafe-fixes
# 4. Apply unsafe fixes after review
ruff check . --fix --unsafe-fixesCI/CD Pipeline
# In CI, never apply unsafe fixes automatically
- name: Lint with auto-fix
run: ruff check . --fix # Safe fixes only
# Check for remaining issues
- name: Check lint
run: ruff check .Pre-commit Hook
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.0
hooks:
- id: ruff
args: [--fix] # Safe fixes onlyFor unsafe fixes in pre-commit (use with caution):
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.0
hooks:
- id: ruff
args: [--fix, --unsafe-fixes]JSON Output
When using JSON output format, all fixes (safe and unsafe) are always displayed:
ruff check . --output-format jsonThe safety of each fix is available in the applicability field:
{
"code": "F401",
"message": "'os' imported but unused",
"fix": {
"applicability": "safe",
"message": "Remove unused import: `os`",
"edits": [...]
}
}Possible applicability values:
safe: Safe to apply automaticallyunsafe: May change behaviordisplay-only: For informational purposes only
Troubleshooting
Fix Not Being Applied
1. Rule is in unfixable list:
ruff check --show-settings | grep unfixable2. Fix is unsafe and --unsafe-fixes not specified:
ruff check . --unsafe-fixes # Check what's available3. Rule doesn't support fixing:
ruff rule F401 # Check if rule supports auto-fixUnexpected Changes After Fix
1. Review what was changed:
git diff2. Check if unsafe fix was applied:
# Run again to see if issues remain
ruff check .3. Revert and be more selective:
git checkout -- .
ruff check . --diff # Preview firstPreventing Specific Fixes
[tool.ruff.lint]
# Don't fix these even with --fix
unfixable = ["F401"]Or use per-file configuration:
[tool.ruff.lint.per-file-ignores]
# In these files, F401 violations are ignored entirely
"__init__.py" = ["F401"]Best Practices
1. Start Conservative: Begin with safe fixes only 2. Review First: Use --diff before applying any fixes 3. Test After Fixing: Run tests after applying fixes 4. Pin Rule Versions: Ensure consistent behavior across environments 5. Document Decisions: Comment why certain rules are in unfixable 6. Team Agreement: Align on fix safety configuration with your team 7. CI Safety: Never use --unsafe-fixes in automated pipelines
Ruff Linting Pitfalls
Common issues and solutions when using Ruff for Python linting.
Configuration Issues
Conflicting Formatter and Linter Rules
Problem: Ruff linter and Ruff formatter (or Black) have conflicting rules.
Symptoms:
- Formatter changes trigger linter violations
- Linter auto-fix changes trigger formatter violations
- Endless loop of fixes
Solution: Disable linter rules that conflict with formatter:
[tool.ruff.lint]
ignore = [
"E501", # Line too long - formatter handles this
"W291", # Trailing whitespace - formatter handles this
"W292", # No newline at end of file - formatter handles this
"W293", # Blank line contains whitespace - formatter handles this
]Or use the recommended approach - let Ruff manage both:
[tool.ruff]
line-length = 88
[tool.ruff.lint]
select = ["E", "F", "UP", "B", "SIM", "I"]
ignore = ["E501"] # Formatter handles line length
[tool.ruff.format]
quote-style = "double"
indent-style = "space"Configuration Not Being Applied
Problem: Changes to configuration don't seem to take effect.
Causes: 1. Wrong configuration file location 2. Syntax errors in TOML 3. Wrong section names 4. Cached results
Solutions:
1. Verify configuration file location:
ruff check . --show-settings2. Check for TOML syntax errors:
python -c "import tomllib; tomllib.load(open('pyproject.toml', 'rb'))"3. Ensure correct section names:
# Correct
[tool.ruff.lint]
select = ["E", "F"]
# Wrong
[tool.ruff]
lint.select = ["E", "F"] # This doesn't work4. Clear cache:
rm -rf .ruff_cache
ruff check .Per-file Ignores Not Working
Problem: Per-file ignore patterns don't match expected files.
Solution: Use correct glob patterns:
[tool.ruff.lint.per-file-ignores]
# Match any file named __init__.py
"__init__.py" = ["F401"]
# Match all Python files in tests directory (recursive)
"tests/**/*.py" = ["S101"]
# Match files in specific directories
"**/{tests,docs,tools}/*" = ["E402"]
# Match specific file
"src/generated/parser.py" = ["E501", "F401"]Rule Selection Issues
Too Many Violations Initially
Problem: Enabling rules creates hundreds/thousands of violations.
Solution: Start minimal and expand gradually:
# Week 1: Start here
[tool.ruff.lint]
select = ["E", "F"]
# Week 2: Add import sorting
select = ["E", "F", "I"]
# Week 3: Add pyupgrade
select = ["E", "F", "I", "UP"]
# Week 4+: Continue expanding
select = ["E", "F", "I", "UP", "B", "SIM"]Rules Too Strict for Project
Problem: Some rules don't fit the project's coding style.
Solution: Ignore specific rules rather than disabling entire categories:
[tool.ruff.lint]
select = ["E", "F", "B", "SIM"]
ignore = [
"SIM108", # Use ternary operator - sometimes if/else is clearer
"B008", # Function call in default argument - needed for FastAPI
]Missing Rules After Migration from Flake8
Problem: Some Flake8 rules aren't being enforced after switching to Ruff.
Solution: Explicitly enable equivalent rule sets:
[tool.ruff.lint]
select = [
# Core Flake8
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # Pyflakes
# Common plugins you may have been using
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"DTZ", # flake8-datetimez
"I", # isort
"S", # flake8-bandit
"T20", # flake8-print
]Check Ruff rules documentation for Flake8 equivalents.
Fix-Related Issues
Unsafe Fixes Breaking Code
Problem: Auto-fix with --unsafe-fixes changed code behavior.
Symptoms:
- Tests fail after auto-fix
- Runtime errors after auto-fix
- Different exception types raised
Solution: Be cautious with unsafe fixes:
# Preview changes first
ruff check . --diff --unsafe-fixes
# Apply fixes selectively
ruff check . --fix # Safe only first
# Review and apply unsafe fixes manually
ruff check . --show-fixes --unsafe-fixesOr limit which rules can be fixed:
[tool.ruff.lint]
fixable = ["ALL"]
unfixable = [
"F401", # Don't auto-remove unused imports
"F841", # Don't auto-remove unused variables
"B", # Don't auto-fix bugbear rules
]Auto-fix Removing Important Comments
Problem: Auto-fix removes comments along with code.
Cause: When removing unused code, associated comments may be removed.
Solution: Review diff before applying fixes:
ruff check . --diff --fix
# Review the output, then:
ruff check . --fixImport Sorting Breaking Circular Imports
Problem: isort rules reorganize imports causing circular import errors.
Solution: Use isort action comments:
# isort: skip_file # Skip entire file
import os
import sys
# isort: off
import module_b # Must be after module_a
import module_a
# isort: on
from package import somethingOr configure known dependencies:
[tool.ruff.lint.isort]
known-first-party = ["mypackage"]
force-sort-within-sections = trueSuppression Issues
noqa Comments Not Working
Problem: # noqa comments don't suppress violations.
Causes: 1. Wrong syntax 2. Wrong rule code 3. Wrong placement
Correct Syntax:
# ✅ Correct
x = 1 # noqa: F841
# ❌ Wrong - missing colon
x = 1 # noqa F841
# ❌ Wrong - wrong case (though Ruff is case-insensitive for noqa)
x = 1 # NOQA: f841 # This actually works
# ✅ Multiple rules
x = 1 # noqa: F841, E501Multiline Strings: Put noqa at end of string:
"""Long docstring...
that spans multiple lines...
""" # noqa: E501Import Blocks: Put noqa on first import:
import os # noqa: I001
import abcToo Many noqa Comments
Problem: Codebase has excessive noqa comments hiding real issues.
Solution:
1. Audit unused noqa comments:
ruff check . --extend-select RUF100
ruff check . --extend-select RUF100 --fix # Remove unused noqa2. Review and reduce noqa usage:
# Find all noqa comments
grep -r "# noqa" src/3. Consider adjusting rules instead:
# Instead of many noqa comments for same rule
[tool.ruff.lint]
ignore = ["E501"] # Ignore globally
# Or per-file
[tool.ruff.lint.per-file-ignores]
"legacy/**/*.py" = ["E501"]Performance Issues
Slow on Large Codebase
Problem: Ruff takes too long on large projects.
Causes: 1. Linting .venv or node_modules 2. Very large files 3. Recursive symlinks
Solution:
[tool.ruff]
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".mypy_cache",
".nox",
".pants.d",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"venv",
"vendor",
]
# Extend default excludes rather than replacing
extend-exclude = ["generated/", "migrations/"]Cache Corruption
Problem: Inconsistent results between runs.
Solution: Clear and rebuild cache:
rm -rf .ruff_cache
ruff check .CI/CD Issues
Different Results Locally vs CI
Problem: CI reports different violations than local development.
Causes: 1. Different Ruff versions 2. Different Python target versions 3. Missing configuration file in CI
Solution:
1. Pin Ruff version:
# pyproject.toml
[project.optional-dependencies]
dev = [
"ruff==0.8.0", # Pin specific version
]2. Ensure consistent target version:
[tool.ruff]
target-version = "py311" # Match CI Python version3. Verify configuration is checked in:
git status pyproject.toml # Should not be ignoredPre-commit Hook Not Fixing Files
Problem: Pre-commit runs ruff but doesn't show fixes.
Solution: Ensure --fix is passed:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.0
hooks:
- id: ruff
args: [--fix] # This is required for auto-fixBest Practices Summary
1. Start Small: Begin with ["E", "F"] and expand gradually 2. Pin Versions: Use exact version in CI and pre-commit 3. Review Fixes: Use --diff before applying fixes 4. Minimize noqa: Prefer configuration over inline suppressions 5. Exclude Properly: Ensure venv and generated code are excluded 6. Sync Team Config: Check pyproject.toml into version control 7. Clear Cache: When debugging, clear .ruff_cache 8. Check Settings: Use ruff check --show-settings to debug configuration
Ruff Linting Quickstart
A comprehensive guide to getting started with Ruff for Python linting.
Installation
Using uv (Recommended)
uv pip install ruffUsing pip
pip install ruffUsing pipx (Global Installation)
pipx install ruffUsing Homebrew (macOS)
brew install ruffVerify Installation
ruff --versionBasic Usage
Lint Current Directory
ruff check .Lint Specific Files or Directories
ruff check src/
ruff check src/main.py src/utils.py
ruff check path/to/project/Auto-fix Issues
# Apply safe fixes only (default)
ruff check . --fix
# Apply all fixes including unsafe ones
ruff check . --fix --unsafe-fixesWatch Mode (Development)
ruff check --watchRuff will automatically re-lint files when they change.
Quick Configuration
Minimal pyproject.toml
[tool.ruff]
line-length = 88
target-version = "py311"
[tool.ruff.lint]
select = ["E", "F"]Recommended pyproject.toml
[tool.ruff]
line-length = 88
target-version = "py311"
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".mypy_cache",
".nox",
".pants.d",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"venv",
]
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"F", # Pyflakes
"UP", # pyupgrade
"B", # flake8-bugbear
"SIM", # flake8-simplify
"I", # isort
]
ignore = [
"E501", # Line too long (handled by formatter)
]
fixable = ["ALL"]
unfixable = []
[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S101"] # Allow assert in tests
[tool.ruff.lint.isort]
known-first-party = ["your_package_name"]Output Formats
Human-Readable (Default)
ruff check .Output:
src/main.py:10:5: F841 Local variable `x` is assigned to but never used
src/utils.py:25:1: E302 Expected 2 blank lines, found 1
Found 2 errors.GitHub Actions Format
ruff check . --output-format githubCreates inline annotations in GitHub pull requests.
JSON Format
ruff check . --output-format jsonUseful for programmatic processing.
SARIF Format
ruff check . --output-format sarifFor security scanning tools and code analysis platforms.
CI Integration
GitHub Actions
name: Lint
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
- name: Install dependencies
run: uv sync
- name: Run Ruff linter
run: uv run ruff check . --output-format github
- name: Run Ruff formatter check
run: uv run ruff format --checkUsing Official Ruff Action
name: Lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/ruff-action@v3GitLab CI
lint:
image: python:3.11
before_script:
- pip install ruff
script:
- ruff check .
- ruff format --checkPre-commit Integration
Install pre-commit
pip install pre-commitConfigure .pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.0
hooks:
# Run the linter with auto-fix
- id: ruff
args: [--fix]
# Run the formatter
- id: ruff-formatInstall Hooks
pre-commit installRun Manually
pre-commit run --all-filesCommon Commands Reference
| Command | Description |
|---|---|
ruff check . | Lint all files |
ruff check . --fix | Lint and apply safe fixes |
ruff check . --fix --unsafe-fixes | Apply all fixes |
ruff check . --diff | Show diff of what would change |
ruff check . --show-fixes | Show available fixes |
ruff check . --statistics | Show violation statistics |
ruff check --watch | Watch mode |
ruff rule F401 | Explain rule F401 |
ruff check . --add-noqa | Add noqa comments to all violations |
ruff format . | Format all files |
ruff format --check | Check if files are formatted |
Editor Integration
VS Code
Install the official Ruff extension.
Settings (.vscode/settings.json):
{
"[python]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.codeActionsOnSave": {
"source.fixAll.ruff": "explicit",
"source.organizeImports.ruff": "explicit"
}
}
}Cursor
Install the Ruff extension from the marketplace.
Settings:
{
"[python]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.codeActionsOnSave": {
"source.fixAll.ruff": "explicit"
}
}
}Neovim (with nvim-lspconfig)
require('lspconfig').ruff.setup({
init_options = {
settings = {
lint = {
enable = true,
},
format = {
enable = true,
},
},
},
})Next Steps
1. Start with minimal rules (E, F) and run ruff check . 2. Fix existing issues with ruff check . --fix 3. Gradually enable more rules (see rule_selection.md) 4. Set up pre-commit hooks for automatic linting 5. Integrate into CI/CD pipeline
Ruff Rule Selection Reference
Comprehensive guide to selecting and configuring Ruff linting rules.
Rule Code Structure
Ruff mirrors Flake8's rule code system. Each rule code consists of:
- Prefix: 1-3 letters indicating the source (e.g.,
Ffor Pyflakes,Efor pycodestyle) - Number: 3 digits identifying the specific rule (e.g.,
401)
Example: F401 = Pyflakes rule 401 (unused import)
Configuration Methods
Using lint.select
The primary way to specify which rules to enable:
[tool.ruff.lint]
select = ["E", "F", "UP", "B"]This replaces the default rule set with only the specified rules.
Using lint.extend-select
Add rules to the existing selection (including defaults):
[tool.ruff.lint]
extend-select = ["UP", "B"] # Add to defaultsUsing lint.ignore
Disable specific rules from the selected set:
[tool.ruff.lint]
select = ["E", "F"]
ignore = ["E501", "F401"]Priority Order
When rules are specified from multiple sources, priority is:
1. CLI arguments (highest priority)
--select,--extend-select,--ignore
2. Current pyproject.toml 3. Inherited pyproject.toml files (lowest priority)
Example:
# Config: select = ["E", "F"], ignore = ["F401"]
ruff check --select F401 # Only enforces F401
ruff check --extend-select B # Enforces E, F, B (except F401)Complete Rule Prefix Reference
Core Linting Rules
| Prefix | Source | Description | Recommended |
|---|---|---|---|
| E | pycodestyle | Style errors (indentation, whitespace) | ✅ Yes |
| W | pycodestyle | Style warnings | ⚠️ Optional |
| F | Pyflakes | Logical errors (undefined names, unused imports) | ✅ Yes |
Code Quality
| Prefix | Source | Description | Recommended |
|---|---|---|---|
| B | flake8-bugbear | Common bugs and design problems | ✅ Yes |
| C4 | flake8-comprehensions | Better comprehensions | ✅ Yes |
| SIM | flake8-simplify | Code simplification | ✅ Yes |
| PIE | flake8-pie | Misc lints | ⚠️ Optional |
| RET | flake8-return | Return statement issues | ⚠️ Optional |
| ARG | flake8-unused-arguments | Unused function arguments | ⚠️ Optional |
Modern Python
| Prefix | Source | Description | Recommended |
|---|---|---|---|
| UP | pyupgrade | Python version upgrades | ✅ Yes |
| FA | flake8-future-annotations | Future annotations | ⚠️ If using from __future__ |
| YTT | flake8-2020 | sys.version checks | ⚠️ Optional |
Import Management
| Prefix | Source | Description | Recommended |
|---|---|---|---|
| I | isort | Import sorting and organization | ✅ Yes |
| ICN | flake8-import-conventions | Import alias conventions | ⚠️ Optional |
| TID | flake8-tidy-imports | Banned imports | ⚠️ Optional |
Type Annotations
| Prefix | Source | Description | Recommended |
|---|---|---|---|
| ANN | flake8-annotations | Type annotation presence | ⚠️ If using type hints |
| TCH | flake8-type-checking | TYPE_CHECKING block usage | ⚠️ If using type hints |
Naming Conventions
| Prefix | Source | Description | Recommended |
|---|---|---|---|
| N | pep8-naming | PEP 8 naming conventions | ⚠️ Optional |
Documentation
| Prefix | Source | Description | Recommended |
|---|---|---|---|
| D | pydocstyle | Docstring conventions | ⚠️ If enforcing docs |
Security
| Prefix | Source | Description | Recommended |
|---|---|---|---|
| S | flake8-bandit | Security issues | ✅ Yes |
Testing
| Prefix | Source | Description | Recommended |
|---|---|---|---|
| PT | flake8-pytest-style | pytest best practices | ✅ If using pytest |
Error Handling
| Prefix | Source | Description | Recommended |
|---|---|---|---|
| EM | flake8-errmsg | Error message formatting | ⚠️ Optional |
| TRY | tryceratops | Exception handling | ⚠️ Optional |
| RSE | flake8-raise | Raise statement issues | ⚠️ Optional |
Logging
| Prefix | Source | Description | Recommended |
|---|---|---|---|
| G | flake8-logging-format | Logging format strings | ⚠️ Optional |
| LOG | flake8-logging | Logging best practices | ⚠️ Optional |
Debugging
| Prefix | Source | Description | Recommended |
|---|---|---|---|
| T10 | flake8-debugger | Debugger imports | ✅ Yes |
| T20 | flake8-print | Print statements | ✅ Yes |
Datetime
| Prefix | Source | Description | Recommended |
|---|---|---|---|
| DTZ | flake8-datetimez | Timezone-aware datetime | ✅ Yes |
Async
| Prefix | Source | Description | Recommended |
|---|---|---|---|
| ASYNC | flake8-async | Async best practices | ⚠️ If using async |
Django
| Prefix | Source | Description | Recommended |
|---|---|---|---|
| DJ | flake8-django | Django best practices | ✅ If using Django |
NumPy/Pandas
| Prefix | Source | Description | Recommended |
|---|---|---|---|
| NPY | NumPy-specific | NumPy deprecations | ✅ If using NumPy |
| PD | pandas-vet | pandas best practices | ✅ If using pandas |
Ruff-Specific
| Prefix | Source | Description | Recommended |
|---|---|---|---|
| RUF | Ruff | Ruff-specific rules | ✅ Yes |
Special
| Prefix | Description |
|---|---|
| ALL | Enable all rules (use with caution) |
Recommended Configurations
Minimal (Getting Started)
For new projects or migrating from no linter:
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"F", # Pyflakes
]Balanced (Recommended for Most Projects)
Good coverage without being overwhelming:
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"F", # Pyflakes
"UP", # pyupgrade
"B", # flake8-bugbear
"SIM", # flake8-simplify
"I", # isort
]
ignore = [
"E501", # Line too long (formatter handles)
]Comprehensive (Strict Projects)
Maximum coverage for strict codebases:
[tool.ruff.lint]
select = [
# Core
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # Pyflakes
# Code quality
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"SIM", # flake8-simplify
"PIE", # flake8-pie
# Modern Python
"UP", # pyupgrade
# Imports
"I", # isort
# Naming
"N", # pep8-naming
# Security
"S", # flake8-bandit
# Debugging
"T10", # flake8-debugger
"T20", # flake8-print
# Datetime
"DTZ", # flake8-datetimez
# Ruff-specific
"RUF", # Ruff rules
]
ignore = [
"E501", # Line length (formatter)
"S101", # Assert usage (tests)
]
[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S101", "PLR2004"]
"conftest.py" = ["S101"]Web Application (FastAPI/Flask)
Optimized for web frameworks:
[tool.ruff.lint]
select = [
"E", "F", "UP", "B", "SIM", "I",
"S", # Security (important for web)
"DTZ", # Timezone-aware dates
"T20", # No print statements
"RUF",
]
ignore = [
"E501",
"B008", # Function calls in default arguments (needed for Depends())
]
[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S101"]
"alembic/**/*.py" = ["E501"]Data Science / ML
For pandas, numpy, and ML projects:
[tool.ruff.lint]
select = [
"E", "F", "UP", "B", "SIM", "I",
"NPY", # NumPy
"PD", # pandas
"RUF",
]
ignore = [
"E501", # Long lines common in data work
"PD901", # Generic variable name 'df'
]
[tool.ruff.lint.per-file-ignores]
"notebooks/**/*.py" = ["E402", "T20"]Library / Package
For reusable Python packages:
[tool.ruff.lint]
select = [
"E", "W", "F", "UP", "B", "SIM", "I",
"N", # Strict naming
"D", # Docstrings
"ANN", # Type annotations
"C4", # Comprehensions
"RUF",
]
ignore = [
"E501",
"D100", # Module docstring
"D104", # Package docstring
"ANN101", # Self type annotation
"ANN102", # cls type annotation
]
[tool.ruff.lint.pydocstyle]
convention = "google" # or "numpy"Per-File Ignores
Override rules for specific files or directories:
[tool.ruff.lint.per-file-ignores]
# Allow assert in tests
"tests/**/*.py" = ["S101", "PLR2004"]
"test_*.py" = ["S101"]
"*_test.py" = ["S101"]
"conftest.py" = ["S101"]
# Allow unused imports in __init__.py (re-exports)
"__init__.py" = ["F401"]
# Allow late imports in specific directories
"**/{tests,docs,tools}/*" = ["E402"]
# Generated code
"**/generated/**/*.py" = ["E501", "F401", "E402"]
# Migrations (often auto-generated)
"**/migrations/**/*.py" = ["E501"]
# Scripts may use print
"scripts/**/*.py" = ["T20"]
# Notebooks exported to Python
"notebooks/**/*.py" = ["E402", "T20", "F401"]Extending with Additional Rules
Adding Rules Incrementally
Start minimal and add rules over time:
# Phase 1: Core
[tool.ruff.lint]
select = ["E", "F"]
# Phase 2: Add quality rules
select = ["E", "F", "B", "SIM"]
# Phase 3: Add import sorting
select = ["E", "F", "B", "SIM", "I"]
# Phase 4: Add pyupgrade
select = ["E", "F", "B", "SIM", "I", "UP"]Using extend-select for Additive Changes
[tool.ruff.lint]
# Keep defaults and add more
extend-select = [
"UP", # pyupgrade
"B", # flake8-bugbear
]Plugin-Specific Configuration
isort Configuration
[tool.ruff.lint.isort]
known-first-party = ["mypackage", "myotherpackage"]
known-third-party = ["requests", "fastapi"]
force-single-line = false
force-sort-within-sections = true
split-on-trailing-comma = true
section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"]pydocstyle Configuration
[tool.ruff.lint.pydocstyle]
convention = "google" # or "numpy" or "pep257"flake8-annotations Configuration
[tool.ruff.lint.flake8-annotations]
allow-star-arg-any = true
ignore-fully-untyped = true
mypy-init-return = trueflake8-bugbear Configuration
[tool.ruff.lint.flake8-bugbear]
extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"]flake8-quotes Configuration
[tool.ruff.lint.flake8-quotes]
docstring-quotes = "double"
inline-quotes = "double"
multiline-quotes = "double"Viewing Available Rules
List All Rules
ruff linter # Show all available rulesExplain a Specific Rule
ruff rule F401 # Show detailed explanation of F401Show Applied Rules
ruff check . --show-settings # Show full configurationShow Statistics
ruff check . --statistics # Show violation counts by ruleMigration from Flake8
Common Flake8 Plugin Mappings
| Flake8 Plugin | Ruff Prefix |
|---|---|
| flake8 core | E, W |
| pyflakes | F |
| flake8-bugbear | B |
| flake8-comprehensions | C4 |
| flake8-simplify | SIM |
| isort | I |
| pyupgrade | UP |
| flake8-bandit | S |
| pep8-naming | N |
| pydocstyle | D |
| flake8-annotations | ANN |
| flake8-pytest-style | PT |
| flake8-print | T20 |
| flake8-debugger | T10 |
Converting .flake8 to pyproject.toml
Before (.flake8):
[flake8]
max-line-length = 88
select = E,F,W,B,B9
ignore = E501,W503
per-file-ignores =
__init__.py:F401
tests/*:S101After (pyproject.toml):
[tool.ruff]
line-length = 88
[tool.ruff.lint]
select = ["E", "F", "W", "B"]
ignore = ["E501"]
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"]
"tests/**/*.py" = ["S101"]