Python Quality - Quick Reference
When NOT to Use This Skill
- SonarQube setup - Use
sonarqubeskill - pytest configuration - Use pytest skills
- Security scanning - Use
python-securityskill
Deep Knowledge: Use
mcp__documentation__fetch_docswith technology:fastapiordjangofor framework-specific patterns.
Tool Overview
| Tool | Focus | Speed | Use Case |
|---|---|---|---|
| Ruff | Linting + formatting | Fastest | All-in-one replacement |
| Black | Formatting only | Fast | Opinionated formatting |
| mypy | Type checking | Medium | Static type analysis |
| Pylint | Deep analysis | Slow | Comprehensive linting |
| isort | Import sorting | Fast | Import organization |
Recommendation: Use Ruff (replaces Black, isort, Flake8, and many Pylint rules).
Ruff Setup (Recommended)
Installation
pip install ruff
# Or with project
pip install "ruff>=0.3.0"
pyproject.toml
[tool.ruff]
target-version = "py312"
line-length = 100
exclude = [".venv", "migrations", "__pycache__"]
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # Pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"ARG", # flake8-unused-arguments
"SIM", # flake8-simplify
"TCH", # flake8-type-checking
"PTH", # flake8-use-pathlib
"ERA", # eradicate (commented code)
"PL", # Pylint
"RUF", # Ruff-specific
]
ignore = [
"PLR0913", # Too many arguments (configure separately)
"PLR2004", # Magic value comparison
]
[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S101"] # Allow assert in tests
[tool.ruff.lint.pylint]
max-args = 5
max-branches = 10
max-returns = 3
[tool.ruff.lint.isort]
known-first-party = ["myapp"]
force-single-line = true
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
docstring-code-format = true
Commands
# Lint
ruff check .
# Fix auto-fixable
ruff check --fix .
# Format
ruff format .
# Check format without changing
ruff format --check .
# Watch mode
ruff check --watch .
mypy Setup
Installation
pip install mypy
pyproject.toml
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
disallow_untyped_decorators = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_unreachable = true
# Per-module overrides
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
[[tool.mypy.overrides]]
module = "migrations.*"
ignore_errors = true
Commands
# Type check
mypy src/
# Show error codes
mypy src/ --show-error-codes
# Generate stubs for library
stubgen -p some_library
Common mypy Errors
| Error | Description | Fix |
|---|---|---|
[arg-type] | Wrong argument type | Fix type or add annotation |
[return-value] | Wrong return type | Fix return or annotation |
[assignment] | Incompatible assignment | Fix type or use cast |
[no-untyped-def] | Missing type annotations | Add annotations |
[union-attr] | Optional access without check | Add None check |
Pylint Setup (Optional - Deep Analysis)
Installation
pip install pylint
pyproject.toml
[tool.pylint.main]
py-version = "3.12"
jobs = 0 # Auto-detect CPU count
ignore-patterns = ["migrations", "__pycache__"]
[tool.pylint.messages_control]
disable = [
"missing-module-docstring",
"missing-class-docstring",
"missing-function-docstring",
"too-few-public-methods",
]
[tool.pylint.design]
max-args = 5
max-locals = 15
max-branches = 10
max-statements = 50
max-attributes = 10
max-public-methods = 20
[tool.pylint.format]
max-line-length = 100
[tool.pylint.similarities]
min-similarity-lines = 5
ignore-imports = true
Commands
# Full check
pylint src/
# Specific file
pylint src/main.py
# Generate config
pylint --generate-rcfile > .pylintrc
Type Hints Best Practices
Basic Types
from typing import Any
# Primitives
name: str = "John"
age: int = 30
active: bool = True
score: float = 95.5
# Collections
names: list[str] = ["Alice", "Bob"]
scores: dict[str, int] = {"Alice": 95, "Bob": 87}
unique_ids: set[int] = {1, 2, 3}
coordinates: tuple[float, float] = (1.0, 2.0)
# Optional (can be None)
middle_name: str | None = None
# Union types
identifier: str | int = "abc123"
# Any (avoid when possible)
data: Any = get_dynamic_data()
Function Signatures
from collections.abc import Callable, Iterable
def greet(name: str) -> str:
return f"Hello, {name}"
def process_items(items: Iterable[int]) -> list[int]:
return [item * 2 for item in items]
def apply_func(func: Callable[[int], int], value: int) -> int:
return func(value)
# Generic functions
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T | None:
return items[0] if items else None
Classes
from dataclasses import dataclass
from typing import Self
@dataclass
class User:
id: int
email: str
name: str | None = None
def with_name(self, name: str) -> Self:
return User(id=self.id, email=self.email, name=name)
Protocols (Structural Typing)
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
def render(item: Drawable) -> None:
item.draw()
# Any class with draw() method works
class Circle:
def draw(self) -> None:
print("Drawing circle")
render(Circle()) # OK
Common Code Smells & Fixes
1. Mutable Default Arguments
# BAD - Mutable default
def append_to(item, target=[]):
target.append(item)
return target
# GOOD - Use None
def append_to(item: str, target: list[str] | None = None) -> list[str]:
if target is None:
target = []
target.append(item)
return target
2. Bare Except
# BAD
try:
do_something()
except:
pass
# GOOD - Specific exceptions
try:
do_something()
except ValueError as e:
logger.warning(f"Invalid value: {e}")
except IOError as e:
logger.error(f"IO error: {e}")
raise
3. God Class
# BAD
class OrderProcessor:
def create_order(self): ...
def send_email(self): ...
def generate_pdf(self): ...
def calculate_tax(self): ...
def update_inventory(self): ...
# GOOD - Single responsibility
class OrderService:
def __init__(
self,
email_service: EmailService,
pdf_generator: PdfGenerator,
tax_calculator: TaxCalculator,
):
self._email = email_service
self._pdf = pdf_generator
self._tax = tax_calculator
def create_order(self, request: OrderRequest) -> Order:
order = self._build_order(request)
order.tax = self._tax.calculate(order)
return order
4. Long Functions
# BAD - 100+ line function
def process_order(order):
# validation
# calculation
# database
# notifications
pass
# GOOD - Extracted functions
def process_order(order: Order) -> ProcessedOrder:
validate_order(order)
total = calculate_total(order)
saved = save_order(order, total)
notify_user(saved)
return saved
5. Magic Numbers
# BAD
if user.age >= 18:
if len(password) >= 8:
pass
# GOOD - Named constants
MINIMUM_AGE = 18
MIN_PASSWORD_LENGTH = 8
if user.age >= MINIMUM_AGE:
if len(password) >= MIN_PASSWORD_LENGTH:
pass
Pre-commit Setup
.pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.8.0
hooks:
- id: mypy
additional_dependencies:
- types-requests
- pydantic
Commands
# Install hooks
pre-commit install
# Run on all files
pre-commit run --all-files
# Update hooks
pre-commit autoupdate
VS Code Settings
// .vscode/settings.json
{
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.ruff": "explicit",
"source.organizeImports.ruff": "explicit"
}
},
"python.analysis.typeCheckingMode": "strict",
"mypy-type-checker.importStrategy": "fromEnvironment"
}
Quality Metrics Targets
| Metric | Target | Tool |
|---|---|---|
| Cyclomatic Complexity | < 10 | Ruff (PLR0912) |
| Cognitive Complexity | < 15 | Ruff (C901) |
| Function Length | < 50 lines | Ruff (PLR0915) |
| Arguments | < 5 | Ruff (PLR0913) |
| Returns | < 3 | Ruff (PLR0911) |
| Type Coverage | 100% | mypy |
CI/CD Integration
GitHub Actions
name: Quality
on: [push, pull_request]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: |
pip install ruff mypy
pip install -e ".[dev]"
- name: Ruff check
run: ruff check .
- name: Ruff format check
run: ruff format --check .
- name: mypy
run: mypy src/
Anti-Patterns
| Anti-Pattern | Why It's Bad | Correct Approach |
|---|---|---|
# type: ignore everywhere | Defeats type checking | Fix types or be specific |
Any overuse | No type safety | Use proper types or TypeVar |
| Mutable default args | Shared state bugs | Use None with check |
Bare except: | Catches everything | Catch specific exceptions |
noqa without code | Ignores all rules | Use noqa: E501 specifically |
| No type hints | Hard to maintain | Add progressive typing |
Quick Troubleshooting
| Issue | Likely Cause | Solution |
|---|---|---|
| mypy can't find module | Missing stubs | Install types-* package |
| Ruff conflicts with Black | Both formatting | Use only Ruff format |
| Type error in library code | Library not typed | Add to mypy ignore list |
| Pre-commit too slow | Running all checks | Use --files for changed only |
| Import order conflicts | Multiple tools | Use only Ruff isort |