Part of Python for Beginners

Python Best Practices - PEP8 Style Guide, Black, Ruff & Mypy Tutorial #41

Sandy LaneSandy Lane

Video: Python Best Practices - PEP8 Style Guide, Black, Ruff & Mypy Tutorial #41 by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

Python Style and Quality: PEP 8, Black, Ruff, mypy

PEP 8 is the official style guide. Black auto-formats; no debate, no config. Ruff is a fast linter — replaces flake8, isort, pylint. mypy (or pyright) does static type checking. Run them in CI; fix the warnings. Series finale.

You've seen the language. The remaining skill is making your code boring — consistent, predictable, easy to skim. Tools do most of the work.

PEP 8: the style guide

PEP 8 is the Python style guide. The rules everyone follows:

# Indentation: 4 spaces (or 2 — pick one and be consistent)
def my_function():
    if True:
        do_something()

# Line length: 79 (PEP 8) or 88 (Black) or 100 (modern). Pick one.

# Naming
my_variable = 1                # snake_case for variables and functions
MY_CONSTANT = 100              # SCREAMING_SNAKE_CASE for constants
class MyClass:                 # CamelCase for classes
  def my_method(self):         # snake_case for methods
    self._private_attr = 1     # leading _ for "internal"

# Imports
import os                      # stdlib
import sys

import requests                # third-party

from .models import User       # local

# One import per line, grouped, alphabetical within groups

The full spec is dense; you'll absorb most of it by example. The tools below enforce it.

Black: the uncompromising formatter

pip install black
black your_module.py
black .                  # whole project

Black reformats your code to its style — no config, no preferences. The result:

# Before
def add(a,b):return a+b
x={'a':1,'b':2,'c':3}

# After (black)
def add(a, b):
    return a + b


x = {"a": 1, "b": 2, "c": 3}

The whole point of Black is not having to think. Run it before every commit. Disagree with a choice? Doesn't matter — consistency beats personal preference.

Configure in pyproject.toml:

[tool.black]
line-length = 100
target-version = ["py310"]

That's the only configuration. By design.

Ruff: the fast linter

pip install ruff
ruff check .             # lint
ruff format .            # format (Black-compatible)
ruff check --fix .       # auto-fix what it can

ruff is a Rust-based linter. It's ~100x faster than flake8 and replaces:

  • flake8 (style)
  • isort (import order)
  • pylint (some rules)
  • pyupgrade (modern syntax)
  • pydocstyle (docstrings)

In pyproject.toml:

[tool.ruff]
line-length = 100
target-version = "py310"

[tool.ruff.lint]
select = ["E", "F", "W", "I", "B", "UP"]
ignore = ["E501"]    # line too long (let formatter handle)

[tool.ruff.format]
quote-style = "double"

E/F/W: pyflakes/pycodestyle. I: isort. B: bugbear (catches common bugs). UP: pyupgrade (modernize syntax).

Ruff in CI catches dead imports, unused variables, mutable default args — bugs and style.

mypy: static type checking

pip install mypy
mypy src/

mypy reads your type hints and reports inconsistencies:

def add(a: int, b: int) -> int:
  return a + b

result: str = add(1, 2)    # mypy: error: incompatible types

In pyproject.toml:

[tool.mypy]
strict = true
python_version = "3.10"

[[tool.mypy.overrides]]
module = ["legacy.*"]
ignore_errors = true

strict = true enables all checks. For a new project, do this from day one. For a legacy project, enable gradually with per-module overrides.

pyright: alternative type checker

Microsoft's checker. Faster than mypy; embedded in VS Code's Pylance.

pip install pyright
pyright src/

Same hints, sometimes different errors — the tools have different opinions on edge cases. Pick one for your project; don't run both.

Pre-commit hooks

Run all the tools on every commit, automatically:

pip install pre-commit

.pre-commit-config.yaml:

repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.5.0
    hooks:
      - id: ruff
      - id: ruff-format
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.10.0
    hooks:
      - id: mypy
pre-commit install
# now hooks run on every git commit

If a hook fails, the commit aborts. Forces clean code.

CI integration

GitHub Actions example:

name: Quality

on: [push, pull_request]

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.12" }
      - run: pip install ruff mypy pytest
      - run: ruff check .
      - run: ruff format --check .
      - run: mypy src/
      - run: pytest

Every PR runs the checks. Failed checks block merge.

Style before vs after

# BEFORE — sloppy
import math,sys
import os

x=10
y = 20
myName="Alice"
UNUSED_VAR =999

def CalculateArea(Width,Height):
  return Width*Height

class student_record:
  def __init__(self,Name,Age,Grade):
    self.Name = Name
    self.Age=Age
    self.Grade = Grade

What's wrong: - import math,sys — multiple imports per line. - Inconsistent spacing. - myName — should be my_name. - UNUSED_VAR — really unused; Ruff flags. - CalculateArea(Width, Height) — function and params should be snake_case. - class student_record: — class should be StudentRecord.

# AFTER — same logic, conventional
import os
import sys

import math


X = 10
Y = 20
MY_NAME = "Alice"


def calculate_area(width: int, height: int) -> int:
  return width * height


class StudentRecord:
  def __init__(self, name: str, age: int, grade: str) -> None:
    self.name = name
    self.age = age
    self.grade = grade

Black + Ruff would do most of this for you. The cognitive load drops to zero.

Docstrings

def calculate_grade(scores: list[int]) -> str:
  """Compute a letter grade from test scores.

  Args:
    scores: List of integer scores 0-100.

  Returns:
    Letter grade A-F based on average.

  Raises:
    ValueError: If the scores list is empty.
  """
  if not scores:
    raise ValueError("scores cannot be empty")
  avg = sum(scores) / len(scores)
  ...

PEP 257 covers docstring conventions. Common styles:

  • Google — readable, popular.
  • NumPy — popular in scientific Python.
  • reST — terse, used in older projects.

Pick one for your project. Keep docstrings in sync with code; mypy + sphinx help catch drift.

A best-practices example

from dataclasses import dataclass

PASSING_GRADE = 60


@dataclass
class Student:
  """A student with test scores."""

  name: str
  scores: list[int]

  def average(self) -> float:
    """Return the mean of all scores."""
    return sum(self.scores) / len(self.scores)

  def letter_grade(self) -> str:
    """Return A-F based on average."""
    avg = self.average()
    for letter, cutoff in [("A", 90), ("B", 80), ("C", 70), ("D", 60)]:
      if avg >= cutoff:
        return letter
    return "F"


def print_report(students: list[Student]) -> None:
  """Print a formatted grade report."""
  print("=== Grade Report ===")
  for s in students:
    status = "Pass" if s.average() >= PASSING_GRADE else "Fail"
    print(f"  {s.name}: {s.average():.1f} ({s.letter_grade()}) - {status}")


if __name__ == "__main__":
  roster = [
    Student("Alice", [95, 88, 92, 97]),
    Student("Bob", [72, 68, 75, 80]),
  ]
  print_report(roster)

What's good: - Dataclass for the model. - Type hints throughout. - Constants in SCREAMING_SNAKE_CASE. - Methods do one thing. - Docstrings on every public function. - if __name__ == "__main__": for the demo. - Everything passes Black, Ruff, and mypy strict.

What "good" looks like

Aim for code that:

  • Reads top to bottom. Top-level, then helpers below.
  • Names things meaningfully. parse_user_email beats process_data.
  • Has consistent style. Tools enforce.
  • Has type hints at boundaries. Function signatures, class attributes.
  • Has tests. Even for prototypes.
  • Has docs. README + docstrings.

A senior dev should be able to skim it and understand the shape without asking questions.

The series ends here

Across 41 lessons, we've covered:

  • Foundations — variables, types, strings, arithmetic, I/O, conditionals, loops.
  • Data structures — lists, tuples, sets, dicts.
  • Functions — def, returns, scope, args/*kwargs, lambdas, comprehensions.
  • Files and errors — open/with, try/except, JSON.
  • Objects — classes, inheritance, polymorphism, encapsulation, abstract classes, operator overloading.
  • Advanced patterns — decorators, iterators, generators, context managers.
  • Real-world tools — regex, venv/pip, unittest, type hints, dataclasses, async/await.
  • Beyond Python — HTTP requests, web scraping, SQLite, SQLAlchemy, CLI tools, FastAPI, Flask, packaging, style.

Enough to read most Python code, build small-to-medium applications, and grow into specialties — data science, web, automation, ML.

Next steps

Pick a project. Some ideas:

  • A web scraper turned data tool. Pull data, clean it, store in SQLite, serve via FastAPI.
  • A CLI utility you'd actually use. A backup script. A note-taker. A habit tracker.
  • A small game. Text adventure, card game, snake.
  • An AI-powered tool. Wrap an LLM API behind a clean Python interface.
  • Contribute to open source. Find an interesting Python repo on GitHub. Read code. Fix a bug.

Building is the only way to consolidate. The lessons are scaffolding; what you build with them is the point.

Common stumbles (final)

Style debates. "Should it be 2 or 4 spaces?" Doesn't matter. Pick one, run Black, move on.

Skipping tests. "I'll add tests later." You won't. Write them as you go.

Adding type hints to nothing. Hints without mypy checking are decoration. Run the checker.

Not using a venv. "It's just a small script." Until it's not. python -m venv .venv from day one.

Avoiding linters because they "complain too much." They're catching real issues. Configure to your taste, then listen.

Premature abstraction. Building a class hierarchy for two cases. Three lines is better than a clever abstraction.

Copy-pasting Stack Overflow without understanding. Read the answer. Type it out. Tweak it. Skim past—you don't learn.

Recap

PEP 8 is the style guide. Black auto-formats — uncompromising and config-free. Ruff lints (and formats) — replaces flake8, isort, pylint, faster than all of them. mypy checks types statically. Run them in pre-commit hooks and CI. Use type hints, docstrings, snake_case for funcs/vars, CamelCase for classes, SCREAMING_SNAKE_CASE for constants. The goal: code that's boring, predictable, and easy for anyone to read.

Series complete. Build something with Python — the only way to truly learn it.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.