Index Ai Academy
Phase 7 · Software engineering · Python in depth

Code style and tools

PEP 8, formatters, linters and type checkers, and how they fit together.

About 35 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

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

  1. PEP 8 in one screen
  2. Three kinds of tool
  3. Making the tools agree
  4. Count the real problems
  5. Running them automatically
  6. Worked example: a quality gate

PEP 8 in one screen

PEP 8 is Python's style guide. The parts that matter most:

  • Names: snake_case for functions, variables and modules; PascalCase for classes; UPPER_CASE for constants; a leading underscore (_helper) for internal names. Names say what something is: unit_price, not up or x2.
  • 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 than if len(items) > 0:, and is None rather 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")
It runs right here in your browser, with nothing to install.

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.

Quick check
What does this print?
import black
print(repr(black.format_str("x = [1,2,3]\nif x :  print( x )\n", mode=black.Mode())))
  1. An error
  2. 'x = [1, 2, 3]\nif x : print( x )\n'
  3. 'x = [1, 2, 3]\nif x:\n print(x)\n'
  4. 'x = [1,2,3]\nif x: print(x)\n'
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.

Try it now

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.

Drill

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)
In the lesson, your code is checked as you write it.

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.yaml file 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. --check makes 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

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)
In the lesson you can change this and run it.

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

Create a free account

Try a quiz question

black and pycodestyle disagree about a line that's 85 characters long. What's the usual fix?

  1. Configure pycodestyle to the same line length as black (88), in the project's config
  2. Stop using black
  3. Shorten every line by hand to 79
  4. Run the tools in a different order

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson