Code Hygiene: Ruff
Developer Environment & Code Hygiene with Ruff
After reading this chapter, you will be able to configure a professional editor with the Language Server Protocol (LSP), enforce automated code formatting and linting with ruff in milliseconds, and integrate pre-commit quality gates into your git repositories.
Mental model
Writing clean, production-grade Python requires two layers of automated feedback: 1. Real-time feedback: Your editor (Zed, VS Code, or Neovim) communicates over the Language Server Protocol (LSP) with an LSP server (such as basedpyright or pyright) to show type errors and syntax diagnostics as you type. 2. Deterministic enforcement: A formatter and linter ensures that every file in your repository conforms to standard formatting rules (line lengths, import sorting, obsolete syntax upgrades, and common bug detection).
Astral’s ruff combines the responsibilities of Black, Flake8, isort, pydocstyle, pyupgrade, and bandit into a single Rust binary that operates 10 to 100 times faster than legacy Python-based linters:
[ Developer in Editor: Zed / VS Code / Neovim ]
│
▼
┌──────────────────────────────────────────────────────────┐
│ Language Server Protocol (LSP) │
│ (Shows real-time parameter hints, completions, & errors) │
└────────────────────────────┬─────────────────────────────┘
│ Save file (Ctrl+S)
▼
┌──────────────────────────────────────────────────────────┐
│ Ruff Engine (Native Rust Binary) │
│ │
│ ┌───────────────────────┐ ┌───────────────────────┐ │
│ │ `ruff format` │ │ `ruff check --fix` │ │
│ │ Replaces Black / │ │ Replaces Flake8, │ │
│ │ yapf. Enforces 4 │ │ isort, pyupgrade, │ │
│ │ spaces, quote styles. │ │ bandit. Fixes bugs. │ │
│ └───────────────────────┘ └───────────────────────┘ │
└────────────────────────────┬─────────────────────────────┘
│ Passes clean code
▼
┌──────────────────────────────────────────────────────────┐
│ Git Pre-Commit Hook & Automated CI Pipeline │
│ (Rejects malformed PRs before code reaches production) │
└──────────────────────────────────────────────────────────┘
Minimal example
To understand what a linter does, look at this intentionally unformatted and messy script.
Save this file as messy_sample.py:
# messy_sample.py
import sys, os
from math import *
import json
def calculate_average( numbers:list ) -> float:
total=0.0
for n in numbers:
total = total+n
return total / len(numbers) if len(numbers)>0 else 0.0
def main():
vals=[10, 20, 30, 40]
print(f"Average: {calculate_average(vals)}")
if __name__ == "__main__":
main()Notice the issues in this file: 1. Unused imports (sys, os, json). 2. Wildcard import (from math import *) which pollutes the namespace. 3. Inconsistent spacing inside parentheses and around operators. 4. Outdated type annotation syntax (list instead of list[float]).
Run the script to verify it still works:
uv run python messy_sample.pyNow, check the file using ruff:
uv run ruff check messy_sample.pyRuff flags the unused imports and wildcard import immediately:
messy_sample.py:2:8: F401 `sys` imported but unused
messy_sample.py:2:13: F401 `os` imported but unused
messy_sample.py:3:1: F403 `from math import *` used; unable to detect undefined names
messy_sample.py:4:8: F401 `json` imported but unused
Now, format the file using ruff format:
uv run ruff format messy_sample.pyRuff normalizes all whitespace, operator spacing, and indentation in under 2 milliseconds.
Worked examples
1. Configuring ruff in pyproject.toml
Rather than relying on ad-hoc CLI flags, professional projects configure linter and formatter rules declaratively inside pyproject.toml.
Here is the standard, production-grade [tool.ruff] configuration for modern Python 3.14 projects:
# pyproject.toml
[project]
name = "production-service"
version = "0.1.0"
requires-python = ">=3.14"
[tool.ruff]
target-version = "py314"
line-length = 100
src = ["src", "tests"]
[tool.ruff.lint]
# Selected rule suites:
# E / W : Pycodestyle (standard PEP 8 formatting errors & warnings)
# F : Pyflakes (detects undefined names, unused variables & imports)
# I : isort (automatic alphabetical import sorting)
# UP : pyupgrade (upgrades syntax to modern Python 3.14 idioms)
# B : flake8-bugbear (detects common bugs and design pitfalls)
# SIM : flake8-simplify (suggests cleaner, idiomatic syntax)
select = ["E", "W", "F", "I", "UP", "B", "SIM"]
ignore = [
"E501", # Line length handled automatically by ruff format
]
[tool.ruff.lint.isort]
combine-as-imports = true
force-single-line = false
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"Why this matters: When anyone on your team runs uv run ruff check or saves a file in their editor, the same exact rules are applied deterministically across Linux, macOS, and Windows.
2. How linters work: building an AST inspector
Linters do not execute your code; they inspect its Abstract Syntax Tree (AST). You can use Python’s built-in ast module to write your own custom code inspector.
Save this file as ast_security_lint.py:
# ast_security_lint.py
import ast
import sys
# Target code string to inspect
CODE_SAMPLE = """
import os
def load_user_input(user_str: str) -> None:
# Danger: eval() allows arbitrary code execution!
result = eval(user_str)
print("Result:", result)
"""
class SecurityASTVisitor(ast.NodeVisitor):
def __init__(self) -> None:
self.violations: list[str] = []
def visit_Call(self, node: ast.Call) -> None:
# Check if the function being called is eval() or exec()
if isinstance(node.func, ast.Name) and node.func.id in {"eval", "exec"}:
self.violations.append(
f"Line {node.lineno}: Dangerous call to '{node.func.id}()' detected!"
)
# Continue traversing child AST nodes
self.generic_visit(node)
def main() -> None:
print("Parsing source code into Abstract Syntax Tree...")
parsed_tree = ast.parse(CODE_SAMPLE)
visitor = SecurityASTVisitor()
visitor.visit(parsed_tree)
if visitor.violations:
print("Security Linter Findings:")
for v in visitor.violations:
print(f" [CRITICAL] {v}")
else:
print("Code passed all AST checks.")
if __name__ == "__main__":
main()Run the script:
uv run python ast_security_lint.pyOutput:
Parsing source code into Abstract Syntax Tree...
Security Linter Findings:
[CRITICAL] Line 6: Dangerous call to 'eval()' detected!
Why this matters: ruff performs this exact AST traversal, but does so in compiled Rust across thousands of files per second.
3. Automated Git pre-commit hooks
To ensure no developer can commit malformed code, configure a pre-commit hook using .pre-commit-config.yaml.
Save this file in the root of your repository as .pre-commit-config.yaml:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.9
hooks:
# Run the linter
- id: ruff
args: [--fix]
# Run the formatter
- id: ruff-formatInstall the pre-commit hook into .git/hooks/:
uv run pre-commit installNow, whenever you run git commit, ruff runs automatically before the commit is created. If formatting is needed, ruff fixes the file and halts the commit so you can stage the clean changes.
Pitfalls
1. Running multiple competing formatters
The Trap: Installing Black, autopep8, and Ruff simultaneously in your editor, resulting in editor stuttering and formatting wars.
The Fix: Disable all legacy formatters. Use ruff exclusively for both linting and formatting. Ruff was explicitly engineered to be a drop-in replacement for Black and isort.
2. Blindly running ruff check --fix without review
The Trap: Running automated fix flags on large repositories and immediately pushing to main without testing.
The Fix: Some lint fixes (like removing unused variables or modifying comprehensions) can change runtime semantics if code relied on side effects. Always run your test suite (uv run pytest) after applying automated fixes.
3. Committing code without running formatting in CI
The Trap: Formatting locally, but forgetting that pull requests submitted by contributors might not have pre-commit hooks installed.
The Fix: Add a GitHub Actions workflow step: uv run ruff check --no-fix and uv run ruff format --check. If any file is unformatted, CI rejects the build.
Exercises
- Create a script with deliberate whitespace and import errors, then use
uv run ruff format --diff <filename>to inspect the unified diff output before writing changes to disk. - In your
pyproject.toml, add ruleB006(flake8-bugbear: mutable default arguments in functions). Write a functiondef append_to(item, target=[])and observe howruff checkcatches the trap. - Configure your primary code editor (VS Code, Zed, or Neovim) to automatically run
ruff formaton file save. Verify that saving a file instantly formats it without lag. - Modify
ast_security_lint.pyto also detect calls toos.system()and raise a warning advising the use ofsubprocess.run().
Further reading
- Official Documentation: Ruff — An extremely fast Python linter and code formatter (
https://docs.astral.sh/ruff/). - PEP 8: Style Guide for Python Code (Guido van Rossum, Barry Warsaw, Nick Coghlan).
- PEP 518: Specifying Minimum Build System Requirements for Python Projects (
pyproject.toml).