Code style and tools
PEP 8, formatters, linters and type checkers, and how they fit together.
Create a free account Try part of it here
Why it matters
Code is read far more often than it's written, usually by someone in a hurry: a teammate reviewing your change, or you in six months. A shared style means nobody spends attention on spacing or naming, and tools can enforce it for free. Linters also catch real bugs, like a misspelled variable or an import you forgot, before the code even runs. Every professional Python team uses these tools, and setting them up is one of the first things you'll do on a new project.
What you'll be able to do
- Follow the core of PEP 8: names, layout, imports and whitespace
- Tell a formatter, a linter and a type checker apart, and what each one catches
- Run black, pyflakes and pycodestyle, and configure them to agree
- Make the tools run automatically: in the editor, before each commit and in CI
What it covers
- PEP 8 in one screen
- Three kinds of tool
- Making the tools agree
- Count the real problems
- Running them automatically
- Worked example: a quality gate
PEP 8 in one screen
PEP 8 is Python's style guide. The parts that matter most:
- Names:
snake_casefor functions, variables and modules;PascalCasefor classes;UPPER_CASEfor constants; a leading underscore (_helper) for internal names. Names say what something is:unit_price, notuporx2. - Layout: 4 spaces per indentation level, never tabs; two blank lines between top-level functions and classes, one between methods; lines kept reasonably short.
- Whitespace: spaces around
=and operators (total = price * qty), but not around=in keyword arguments (round(x, ndigits=2)), and none just inside brackets. - Imports: at the top of the file, one per line, grouped: standard library, then third-party packages, then your own modules, with a blank line between groups.
- Comparisons:
if items:rather thanif len(items) > 0:, andis Nonerather than== None.
Nobody memorizes the whole document. You learn the main points, and let the tools handle the rest, so your attention goes to what the code actually does.
Three kinds of tool
Code-quality tools do three different jobs, and teams usually run all three:
- A formatter rewrites your code's layout into one consistent style. You don't argue with it; you run it. black is the most common for Python.
- A linter reads your code without running it and reports likely mistakes and style problems. pyflakes looks for real bugs: unused imports, undefined names, variables assigned and never used. pycodestyle checks PEP 8 layout rules.
- A type checker like mypy checks that types fit together: that you don't pass a string where a number is expected. The next lesson covers it.
All three run here in the browser:
import io import black import pycodestyle from pyflakes.api import check from pyflakes.reporter import Reporter messy = '''import os, json def Total( items,tax = 0.08 ): subtotal=sum( i["price"]*i["qty"] for i in items ) return round(subtotal*(1+tax),2) print(Total([{"price":10,"qty":3}], taxx)) ''' print(black.format_str(messy, mode=black.Mode())) problems = io.StringIO() check(messy, "shop.py", Reporter(problems, problems)) print(problems.getvalue()) open("shop.py", "w").write(messy) style = pycodestyle.StyleGuide(max_line_length=88) report = style.check_files(["shop.py"]) print(report.total_errors, "style problems")
black fixed the spacing, the indentation and the blank lines in one go. But it didn't touch the function's name, because renaming changes behavior; that's a human's job. pyflakes found the two bugs: os and json are imported and never used, and taxx is a typo for a name that doesn't exist. That would crash at run time, and the linter caught it without running anything.
import black print(repr(black.format_str("x = [1,2,3]\nif x : print( x )\n", mode=black.Mode())))
Show the answer
'x = [1, 2, 3]\nif x:\n print(x)\n'
black adds spaces after commas, removes the space before the colon, moves the body onto its own indented line and strips the spaces inside the brackets.
Formatters fix layout; linters spot likely bugs and style slips; type checkers catch mismatched types. None of them judges whether your logic is right: tests do that.
Making the tools agree
Tools have settings, and two tools with different defaults can disagree. The classic case: black wraps lines at 88 characters, but pycodestyle complains about anything over 79. Teams set both to the same length, and turn off the few pycodestyle checks that conflict with black's choices (E203 and W503). Settings live in the project's pyproject.toml, or in setup.cfg for tools that don't read it:
[tool.black] line-length = 88 [tool.mypy] strict = true
[pycodestyle] max-line-length = 88 ignore = E203, W503
Many teams now use ruff, a single, very fast tool that does the job of pyflakes, pycodestyle, isort (import sorting) and dozens of other checks, plus a black-compatible formatter. The ideas are the same; only the command changes. Whatever the tools, the principle is to agree once, write it in the config file, and let the machine enforce it.
Occasionally a rule is wrong for one particular line, such as a long URL that can't be split. A comment like # noqa: E501 at the end of that line silences that one check for that one line. Use it rarely, and always name the code: a bare # noqa hides every warning on the line, including real bugs you'd want to hear about.
Count the real problems
Use pyflakes to check CODE, and set messages to the list of messages it reports, each without the file-and-line prefix (the text after the last : ).
import io from pyflakes.api import check from pyflakes.reporter import Reporter CODE = '''import sys import math def area(r): result = math.pi * r ** 2 return reslt ''' messages = [] print(messages)
Running them automatically
Tools only help if they run. Three places make that automatic:
- In the editor. VS Code's Python extension can format with black on every save and underline lint problems as you type. Most mistakes are fixed seconds after they're made.
- Before each commit. The pre-commit tool runs a list of checks whenever you
git commit, and stops the commit if one fails, so badly formatted or broken code never enters the history. Its configuration is a small.pre-commit-config.yamlfile listing the tools. - In CI. The project-structure lesson's CI workflow can run
black --check ., the linters and the tests on every push and pull request.--checkmakes black report files it would change instead of changing them, and fail if there are any. Then a reviewer never has to comment on formatting again.
A tidy codebase isn't about taste. When every file looks the same, real changes stand out in reviews. Bugs that tools can find get found automatically. And people spend their attention on what the code does.
Worked example: a quality gate
Write the check a CI job would run: format with black's check mode, lint with pyflakes and pycodestyle using shared settings, and report one pass or fail.
import io import black import pycodestyle from pyflakes.api import check FILES = { "prices.py": 'def with_tax(price, rate=0.08):\n return round(price * (1 + rate), 2)\n', "report.py": 'import json\n\n\ndef summary(rows):\n total=sum(r["qty"] for r in rows)\n return f"{len(rows)} rows, {total} units"\n', } def quality_gate(files): problems = [] for name, source in files.items(): if black.format_str(source, mode=black.Mode()) != source: problems.append(f"{name}: black would reformat it") flakes = io.StringIO() if check(source, name, __import__("pyflakes.reporter", fromlist=["Reporter"]).Reporter(flakes, flakes)): problems += flakes.getvalue().splitlines() open(name, "w").write(source) style = pycodestyle.StyleGuide(max_line_length=88, ignore=["E203", "W503"], quiet=True) if style.check_files([name]).total_errors: problems.append(f"{name}: pycodestyle found problems") return problems found = quality_gate(FILES) print("PASS" if not found else "FAIL") for p in found: print(" ", p)
The gate never changes files; it only reports, which is what CI should do. prices.py is clean. report.py fails three ways: black would add spaces around =, pyflakes reports the unused json import, and pycodestyle flags the same spacing. Collecting every problem before failing, rather than stopping at the first, lets the author fix them all at once. In a real project this is three lines in a CI workflow, but the logic is exactly this.
Practice what you just read
- 2 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 5 review cards for spaced repetition
Try a quiz question
black and pycodestyle disagree about a line that's 85 characters long. What's the usual fix?