19 Code Style
Prerequisites (read first if unfamiliar): Chapter 17.
See also: Chapter 32, Chapter 33, Chapter 31.
Purpose

Code style is one of those skills people assume you will absorb by osmosis. You will not. Without being taught, most novices write code that is technically correct but visually noisy — inconsistent indentation, trailing whitespace, unused imports, variable names that start with a lowercase letter here and an uppercase letter there. In solo work, this is a cosmetic problem. In collaborative work, it becomes a real one: code review time is wasted on style nitpicks, diffs are noisy, and merge conflicts multiply.
The fix is simple and has two parts:
- A formatter rewrites your code to a canonical style automatically. You stop making style decisions.
- A linter scans your code for problems that a formatter cannot fix — unused variables, shadowed builtins, likely bugs, style guide violations.
Together, these two tools take about ten minutes to set up and pay for themselves within a week. This chapter teaches you the two tools that are now standard in Python: black (formatter) and ruff (linter and optional formatter). You will be up and running by the end.
Learning objectives
By the end of this chapter, you should be able to:
- Explain the difference between a formatter and a linter.
- Install and run
blackto format a Python file or project. - Install and run
ruff checkto lint a Python file or project, andruff formatas a faster alternative toblack. - Configure style rules in
pyproject.toml. - Integrate a formatter with VS Code / PyCharm so your editor formats on save.
- Explain what PEP 8 is and why you will almost never need to read it directly.
- Recognize when a style rule is worth fighting and when it is worth accepting.
- Set up a minimum viable linting workflow for a student project.
Running theme: make the machine handle style so humans can focus on logic
If you find yourself debating spaces or import order with a collaborator, you have already lost. Delegate the decision to a formatter and a linter, commit their config to git, and move on to the actual code review.
19.1 PEP 8 in one paragraph
PEP 8 is the official Python style guide. It is short and sensible: 4-space indents, 79-character lines (most people now use 88 or 100), snake_case for functions and variables, PascalCase for classes, UPPER_SNAKE for constants, imports at the top, one blank line between functions, two between top-level definitions, no trailing whitespace. The modern formatters (black, ruff format) implement PEP 8 with a handful of opinionated choices baked in, so you almost never need to read PEP 8 yourself — you just run the formatter and the code comes out compliant.
19.2 Formatters vs. linters
A formatter rewrites your code to look a certain way. It will not change what the code does. You run it, the file is rewritten, and the diff is entirely cosmetic. black is the canonical Python formatter.
A linter reads your code and reports problems — without changing it. Problems can be anything from “you imported os but never used it” to “this function might return None on a path your caller doesn’t handle.” ruff check and pylint are linters.
They are complementary. Formatters fix style; linters find mistakes.
19.3 black: the boring formatter
black is called “the uncompromising Python code formatter” for a reason: it does not take many options. You install it, you run it, your code is black-formatted. No discussions about whether to use single or double quotes. Black picks; you move on.
Install it in your venv (see Chapter 15):
python -m pip install blackRun it on a single file:
black src/analysis.pyRun it on a whole project:
black .Run it in check-only mode (no changes, just exit non-zero if files would change — useful in CI):
black --check .See what it would change, without changing it:
black --diff src/analysis.pyConfiguration
Black takes its config from pyproject.toml:
[tool.black]
line-length = 100
target-version = ["py311"]That is almost everything you can configure. The point of black is that it is opinionated; you are not meant to tune it much.
19.4 ruff: the fast linter (and formatter)
ruff is a modern Python linter written in Rust. It is 10–100× faster than the older tools (flake8, pylint), implements hundreds of rules, and increasingly doubles as a formatter compatible with black. If you are starting a new project today, the straightforward choice is “use ruff for both linting and formatting.”
Install:
python -m pip install ruffLint a file or project:
ruff check .Auto-fix problems that are safely auto-fixable (unused imports, trailing whitespace, import order):
ruff check --fix .Format (this is a newer feature; ruff format is drop-in compatible with black):
ruff format .Configuration
Like black, ruff reads from pyproject.toml:
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.ruff.lint]
# Which rule sets to enable. See https://docs.astral.sh/ruff/rules/
select = [
"E", # pycodestyle errors (PEP 8)
"F", # pyflakes (logic errors)
"I", # isort (import order)
"B", # bugbear (likely bugs)
"UP", # pyupgrade (modernize syntax)
]
ignore = [
"E501", # line too long — let the formatter handle it
]
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"] # allow unused imports in package init filesA sensible starting config for a student project: select E, F, I, B, UP, ignore line-length (the formatter controls that), and let per-file ignores handle the rest as you run into them.
19.5 Editor integration: format on save
The real magic happens when your editor runs the formatter automatically on every save. Your code is never in an unformatted state for more than a split second, and you stop thinking about style entirely.
VS Code
Install the Black Formatter or Ruff extension, then add to your workspace settings.json:
{
"editor.formatOnSave": true,
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit"
}
}
}Now every time you hit Cmd+S / Ctrl+S, Ruff formats and fixes imports and unused variables.
PyCharm
Settings → Tools → External Tools → add Black or Ruff. Or use the built-in integrations: Settings → Tools → Black / Ruff, and enable “On save.”
Jupyter
Notebooks are harder to lint because the cell model breaks some assumptions. jupyter_black is a drop-in package:
python -m pip install jupyter_blackIn the first cell of a notebook:
%load_ext jupyter_blackEvery code cell is formatted on execution from then on.
19.6 Rule sets: what do E, F, I, B, UP mean?
ruff groups rules into sets that match the old pre-ruff tools they replace. The most useful ones for a student project are:
| Code | Source | What it catches |
|---|---|---|
E, W |
pycodestyle | PEP 8 style (indentation, whitespace, line length) |
F |
pyflakes | unused imports, undefined names, duplicate arguments |
I |
isort | import ordering |
B |
flake8-bugbear | likely bugs (mutable default args, unused loop variables) |
UP |
pyupgrade | modernize syntax (f-strings over .format, etc.) |
SIM |
flake8-simplify | simpler equivalent constructs |
ANN |
flake8-annotations | missing type hints |
D |
pydocstyle | docstring conventions |
You do not need all of them. Start with E, F, I, B, UP. Add more as the project grows.
19.7 When to override a rule
Most linter rules are helpful and you should accept them. Some will not apply to your project. There are three ways to override:
Globally, in pyproject.toml:
[tool.ruff.lint]
ignore = ["E501"] # line too longPer-file, in pyproject.toml:
[tool.ruff.lint.per-file-ignores]
"tests/*" = ["E501", "S101"]Per-line, with a # noqa comment:
from legacy_code import UNUSED # noqa: F401Use # noqa sparingly. Every one is a small promise that a human thought about it and decided the rule did not apply. Unexplained # noqa comments accumulate and become dead weight.
19.8 Stakes and politics
Linters and formatters are unusually political tools because their job is to enforce a single answer to questions that have many reasonable answers. PEP 8, the style guide most Python linters encode, was written by Guido van Rossum and the Python core team in 2001 — a small group with strong opinions, working in a particular community at a particular moment. The choices that froze in PEP 8 (4-space indentation, snake_case for functions, 79-character line length) became “Pythonic” by social rather than technical means; they could have been different and the language would still work fine.
Two consequences worth naming. First, what counts as readable code. A formatter like Black ends arguments by enforcing one style across every project that uses it. That is genuinely useful — it removes the cost of stylistic bikeshedding and makes diffs cleaner — but it also flattens the legitimate variation that different communities and individuals develop, and it bakes the preferences of the formatter’s authors into every file. Second, who maintains the tools. Ruff, the fastest-growing linter in the ecosystem, is built and primarily maintained by Astral, a venture-backed company; Black is run by a small volunteer group. The choices about which rules ship as defaults are influenced by who gets to make them, and the trajectory has been toward more centralization, not less.
See Chapter 8 for the broader framework. The concrete prompt to carry forward: when you adopt a linter or formatter, you are adopting someone else’s idea of what code should look like. That trade is usually worth it, but it is a trade — and it is worth knowing whose preferences you have inherited.
19.9 Worked examples
Setting up a new project
python -m venv .venv
source .venv/bin/activate
python -m pip install ruff
# Create a minimal pyproject.toml
cat > pyproject.toml <<'EOF'
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP"]
ignore = ["E501"]
[tool.ruff.format]
quote-style = "double"
EOF
# Run once on the existing code
ruff check --fix .
ruff format .
git add pyproject.toml
git commit -m "Add ruff config and initial formatting pass"Now ruff check will run instantly on every subsequent change.
Fixing an unused import
Before:
import os
import sys
import pandas as pd
def main():
df = pd.read_csv("data.csv")
print(df.head())Running ruff check --fix main.py rewrites the file to:
import pandas as pd
def main():
df = pd.read_csv("data.csv")
print(df.head())os and sys were unused; ruff removed them automatically.
Catching a real bug
# bug.py
def greet(name, greetings=[]):
greetings.append(f"Hello, {name}!")
return greetingsRunning ruff check bug.py:
bug.py:1:24: B006 Do not use mutable data structures for argument defaults
The linter caught a classic Python gotcha: a mutable default argument is shared across calls, so greet("Alice") followed by greet("Bob") returns ["Hello, Alice!", "Hello, Bob!"], not ["Hello, Bob!"]. The fix is:
def greet(name, greetings=None):
if greetings is None:
greetings = []
greetings.append(f"Hello, {name}!")
return greetingsThis is a real bug. You would find it in production, not in your head. A linter finds it in 20 milliseconds.
19.10 Templates
A minimal pyproject.toml for a student project:
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP"]
ignore = ["E501"]
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"]
"tests/*" = ["B011"]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"A one-line CI check to add to your GitHub Actions workflow (see Chapter 33):
- name: Lint
run: |
python -m pip install ruff
ruff check .
ruff format --check .Wire this into the same CI workflow you use for any other automated checks and every push gets automatic style enforcement.
19.11 Exercises
- Install
ruffin a venv, pick one of your own Python files, and runruff checkon it. How many issues does it report? Skim the output and pick two you do not understand. - Run
ruff check --fixon the same file. Read the diff. Did anything you cared about change? - Run
ruff formaton the same file. Read the diff. How many changes are cosmetic vs. substantive? - Create a
pyproject.tomlwith the template from section 9 and commit it. - Wire up format-on-save in your editor. Make a one-character change and save. Confirm ruff reformatted the file.
- Deliberately introduce a mutable default argument (like the example in section 8) and confirm that
ruff checkreports it. - Add
ruff checkto your CI workflow. Intentionally break formatting in a commit and confirm CI fails.
19.12 One-page checklist
- Install
ruffin every Python venv. - Commit a
pyproject.tomlwith[tool.ruff]configuration. - Enable format-on-save in your editor.
- Run
ruff check .andruff format .before every commit (or automate with the pre-commit hook described in Chapter 33). - Use
--fixto auto-fix safe issues; read everything else manually. - Prefer
ruff formatoverblackin new projects; they produce near-identical output,ruffis just faster. - Use
# noqa: RULEsparingly and always with the rule code. - Wire
ruff check --format --checkinto CI so style drift cannot land on main. - Do not debate style with collaborators. Configure the formatter, run it, move on.
- Python, PEP 8 — Style Guide for Python Code — the authoritative style reference most Python linters encode; worth reading once end-to-end so you know what your tools are enforcing.
- Astral, Ruff documentation — the canonical reference for the modern all-in-one linter and formatter; the rules pages are organized by category and worth scanning when you adopt new rule sets.
- Black documentation — the opinionated formatter that eliminates style arguments by being unconfigurable; the philosophy notes (“Why Black?”) explain the trade-off cleanly.
- mypy, Documentation — Python’s standard static type checker; pairs with linters once you start adding type hints.
- pre-commit — the framework most projects use to run linters and formatters automatically before each commit; covered in Chapter 33.
- EditorConfig — a small cross-language standard for indent, line ending, and charset settings that every modern editor reads; useful when you collaborate across editors.
- Google, Python Style Guide — Google’s company-internal Python style; a useful comparison point for “what if a different group had written PEP 8?”