Formatting and linting with Ruff
Tests check that your code does the right thing. Formatters and linters check that it is written in a way other people can read — and catch a surprising number of real bugs before a test ever runs.
The argument nobody should be having
How many spaces around an operator. Single or double quotes. Where to break a long line. Every team that discusses these wastes hours, and the outcome affects nothing.
A formatter ends the discussion by deciding for everyone. That is its main value: not that its choices are optimal, but that they are consistent and not yours to argue about.
Ruff
Ruff does both formatting and linting, is extremely fast, and has largely replaced the older combination of Black, Flake8 and isort. It also understands their configuration, so you will find it familiar if you meet those.
python -m pip install ruff
ruff format . reformat everything
ruff check . report problems
ruff check --fix . fix what can be fixed automatically
That is essentially the whole interface.
What the formatter does
def calculate( amount,rate = 0.18 ):
total=amount+(amount*rate)
return round( total,2 )
ruff format .
def calculate(amount, rate=0.18):
total = amount + (amount * rate)
return round(total, 2)
Spacing, quote style, line length, trailing commas, indentation — all decided. The point is not that this version is beautiful; it is that every file in the project now looks the same, so differences in a diff are differences in behaviour rather than in whitespace.
Run it on save. Module 1 had you tick "Format On Save" in VS Code; point it at Ruff and formatting stops being something you think about.
What the linter catches
More interesting, because some of it is genuinely bugs:
import json
import os
def process(data):
results = []
for item in data:
if item["status"] == "active":
results.append(item)
return results
F401 [*] `json` imported but unused
F401 [*] `os` imported but unused
Unused imports are noise and occasionally the remains of deleted code.
It also finds:
- Undefined names — the typo that would be a
NameErrorat runtime, caught now - Unused variables — often an assignment that should have been used
- Shadowed built-ins — the
list = [...]problem from module 5 - Mutable default arguments — module 5's trap, flagged before it bites
- Bare
except:— module 6's warning, enforced - Comparisons to
Nonewith==— theis Nonerule - f-strings with no placeholders — usually a forgotten
{}
That list is most of the traps this course has warned about, checked automatically. A linter is the accumulated experience of a lot of people who made these mistakes first.
Configuring it
In pyproject.toml, from module 8:
[tool.ruff]
line-length = 88
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP"]
ignore = ["E501"]
The rule families worth enabling:
| Code | Covers |
|---|---|
E, W |
style issues from PEP 8 |
F |
real errors — undefined names, unused imports |
I |
import sorting |
B |
likely bugs, including mutable defaults |
UP |
outdated syntax that has a modern form |
F and B are the ones that find bugs. Start with those and E, and add more
once the codebase is clean.
Ignore rules deliberately, not reflexively. When you disagree with one, silence that rule in the config with a comment saying why. A line-level override is available when a rule is wrong just once:
import config # noqa: F401 — imported for its side effects
A bare # noqa with no rule code silences everything on that line, which
defeats the purpose.
PEP 8
The Python style guide, and what most of these rules implement. Worth reading once, in full — it is short and explains its reasoning.
The parts you will use daily:
snake_casefor functions and variables,CapWordsfor classes,UPPER_CASEfor constants- Four spaces per indent, never tabs
- Two blank lines between top-level definitions, one between methods
- Imports at the top, grouped: standard library, third party, your own
- Spaces around operators, none inside brackets
You do not need to memorise it. Run the formatter and you comply by default.
Type checking
Module 5 introduced type hints and mentioned mypy. It belongs in the same
workflow:
python -m pip install mypy
mypy src
def apply_discount(amount: float, percent: float) -> float:
...
apply_discount("1000", 10)
error: Argument 1 has incompatible type "str"; expected "float"
Found without running anything. On a codebase with hints throughout, this
catches a real class of bug — particularly the None that a function might
return and the caller forgot to handle:
user = find_user(users, email) # returns dict | None
print(user["name"])
error: Value of type "dict[str, Any] | None" is not indexable
That is module 5's dict | None hint doing its job. The hint was documentation;
the checker makes it enforcement.
Adding mypy to an existing untyped project is a slog. Adding it at the start costs almost nothing.
Putting it in the workflow
The habit that keeps this useful rather than annoying:
ruff format .
ruff check --fix .
mypy src
pytest
Four commands, a few seconds, before you commit.
You can automate it with pre-commit hooks so it runs on every commit, and in CI so a pull request cannot merge while failing. Both are outside this course — what matters now is the habit. Formatting and linting on save, tests before you commit.
The honest limits
None of this makes code correct.
A formatter cannot tell you a name is misleading. A linter cannot tell you a function does too much. A type checker cannot tell you the logic is wrong. They catch the mechanical mistakes so that review and testing can spend their attention on the things that actually need judgement.
They are a floor, not a ceiling.
Check your work
What ruff check reports on the deliberately bad code: F401 unused
imports, E722 bare except, B006 mutable default argument, E711 comparison
to None. Those are the traps this course has warned about since module 5,
caught automatically.
What --fix cannot fix. Unused imports and import order it can. The bare
except: and the mutable default it cannot — because the correct replacement
depends on intent, and a tool guessing would be worse than a human deciding.
The config.
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP"]
F and B are the ones that find real bugs. E is style, I sorts imports,
UP modernises outdated syntax.
Silencing a rule properly.
import config # noqa: F401 — imported for its side effects
With the code and a reason. A bare # noqa silences everything on the line,
which defeats the purpose.
mypy.
error: Argument 1 to "apply_discount" has incompatible type "str"; expected "float"
Found without running anything. And on dict | None:
error: Value of type "dict[str, Any] | None" is not indexable
That is module 5's hint doing its job — documentation turned into enforcement.
The honest limit. None of these make code correct. A formatter cannot tell you a name is misleading, a linter cannot tell you a function does too much, a type checker cannot tell you the logic is wrong. They are a floor, not a ceiling — they clear the mechanical mistakes so review and testing can spend attention on what needs judgement.
Practice
- Install Ruff. Run
ruff formaton badly formatted code and read the diff. - Add unused imports, a bare
except:, a mutable default and== None. Runruff checkand read every message. - Run
ruff check --fixand see which it fixed and which it did not. Ask yourself why the unfixable ones need a human. - Add a
[tool.ruff]section topyproject.tomlwithselect = ["E", "F", "I", "B", "UP"]. - Deliberately break a rule and silence it with
# noqaplus the code and a reason. - Configure VS Code to format with Ruff on save.
- Install mypy and run it on a file with hints. Call a function with the wrong type and read the error.
- Write a function returning
dict | None, use the result without checking, and let mypy catch it. - Run all four commands on a project of yours and fix everything they report.
Next: Git, so the working version is never the only version.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship