Python Best Practices - PEP8 Style Guide, Black, Ruff & Mypy Tutorial #41
Video: Python Best Practices - PEP8 Style Guide, Black, Ruff & Mypy Tutorial #41 by Taught by Celeste AI - AI Coding Coach
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_emailbeatsprocess_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.