Python · SQL · Web Dev · Java · AI/ML tracks launching soon — your one platform for all of IT
Advanced+150 XP

Python Best Practices — PEP 8, Clean Code

The conventions that separate readable, maintainable Python from code that works but nobody wants to touch.

30 min August 2026
// Part 01 — What PEP 8 Actually Is

A Style Guide, Not a Language Rule

PEP 8 is Python's official style guide — a document, not a language feature. Code that violates every PEP 8 rule still runs perfectly fine; the value of PEP 8 is entirely social and practical: it means any two Python developers, from different companies, different countries, who have never met, can read each other's code without a mental translation step, because formatting conventions are shared rather than personal.

A quick example of what PEP 8 actually governs
# PEP 8 compliant
def calculate_total(price, tax_rate):
    return price * (1 + tax_rate)


# Not PEP 8 compliant — still runs identically, just harder for anyone else to read
def calculateTotal(price,tax_rate):
        return price*(1+tax_rate)
// Part 02 — Naming Conventions

snake_case, PascalCase, and SCREAMING_SNAKE_CASE

The four naming conventions and where each applies
# snake_case — functions, variables, methods, modules
def calculate_total(price, tax_rate):
    user_name = "Asha"

# PascalCase — classes
class PaymentProcessor:
    ...

# SCREAMING_SNAKE_CASE — constants (values that never change)
MAX_RETRIES = 3
DEFAULT_TIMEOUT_SECONDS = 30

# _leading_underscore — internal/non-public (covered fully in the Encapsulation module)
class Account:
    def __init__(self):
        self._internal_cache = {}

These are not arbitrary — camelCase for functions and snake_case for classes would technically run without any error, but it would instantly signal "this code was written by someone unfamiliar with Python conventions" to any experienced reviewer, in the same way unusual punctuation stands out in prose even when the sentence is grammatically valid.

🎯 Pro Tip
Names should describe what a value IS or what a function DOES, not how it is implemented. user_list is weaker than users (the type is usually obvious from context or a type hint); calculate_and_return_total is weaker than just calculate_total (every function "returns" something — the word adds nothing). Favour names a reader can understand without opening the function body.
// Part 03 — Layout Conventions

Line Length, Whitespace, and Blank Lines

Core layout rules
# 4 spaces per indentation level (never tabs) — covered back in Control Flow
if condition:
    do_something()

# Max line length: 79 characters (many teams relax this to 99-120 in practice,
# but PEP 8's own recommendation is 79 — check your project's actual configured limit)

# Two blank lines between top-level function/class definitions
def first_function():
    ...


def second_function():
    ...


class MyClass:
    ...

# One blank line between methods inside a class
class Account:
    def deposit(self, amount):
        ...

    def withdraw(self, amount):
        ...
Whitespace around operators
# Preferred
total = price * quantity
result = (a + b) * (c - d)

# Avoid
total=price*quantity
result = ( a+b ) * ( c-d )
// Part 04 — Import Ordering

Three Groups, in a Consistent Order

The conventional import grouping
# 1. Standard library imports
import json
import os
from datetime import datetime

# 2. Third-party imports
import requests
import pandas as pd

# 3. Local/project imports
from myapp.models import User
from myapp.utils import format_currency

Each group is conventionally separated by a blank line, and alphabetised within the group. Tools like isort automate this entirely — running it as part of a project's formatting pipeline means nobody has to manually maintain import ordering by hand.

// Part 05 — Docstrings

Documenting What a Function Does, Not How

A properly documented function
def calculate_discount(price, percent):
    """Apply a percentage discount to a price.

    Args:
        price: The original price, in dollars.
        percent: The discount percentage, from 0 to 100.

    Returns:
        The discounted price, rounded to 2 decimal places.

    Raises:
        ValueError: If percent is outside the 0-100 range.
    """
    if not 0 <= percent <= 100:
        raise ValueError(f"Invalid discount percent: {percent}")
    return round(price * (1 - percent / 100), 2)

A docstring documents the function's contract — what it expects, what it returns, what can go wrong — not its internal implementation, which the code itself already shows. Several docstring formats exist (Google-style, shown above; NumPy-style; reST) — the specific format matters far less than a team picking one and staying consistent, since tools that auto-generate documentation from docstrings expect one recognisable format throughout a codebase.

// Part 06 — The Zen of Python

import this — A Real Design Philosophy, Not Just a Joke

Running it yourself
>>> import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
...

It reads like a novelty at first, but several of its lines map directly onto concrete patterns already covered throughout this track: "Explicit is better than implicit" is exactly why from module import * is discouraged (the Modules module) — it hides where names actually come from. "Flat is better than nested" is precisely the guard-clause pattern from Control Flow, restructuring deeply nested conditionals into flat, early-return logic. "There should be one — and preferably only one — obvious way to do it" is why idiomatic Python conventions (like preferring if items: over if len(items) > 0:) matter beyond personal taste — consistency across a codebase and across the whole language community is itself valuable.

// Part 07 — Code Smells and When to Break the Rules

Style Guides Are Guidelines, Not Laws

A few common code smells worth recognising
# Smell: a function doing too many unrelated things
def process_order(order):
    validate(order)
    charge_card(order)
    send_email(order)
    update_inventory(order)
    log_analytics(order)
# Better: split into smaller, focused functions — each independently
# testable and each with a name describing exactly one responsibility

# Smell: "magic numbers" with no explanation
if attempts > 3:
    lock_account()
# Better: name the constant, and its meaning becomes self-documenting
MAX_LOGIN_ATTEMPTS = 3
if attempts > MAX_LOGIN_ATTEMPTS:
    lock_account()
🎯 Pro Tip
PEP 8 itself explicitly says consistency within a project sometimes beats blind adherence to the guide. If an existing codebase consistently uses a different convention (a different max line length, a different docstring style), matching the surrounding code is usually more valuable than introducing a one-off "more correct" style that now makes that one function inconsistent with everything around it. The goal is readability and consistency, not rule-following for its own sake.
// Part 08 — Automating It

Linters and Formatters — Tools, Not Memorisation

The standard modern toolchain
pip install black ruff

black your_project/       # auto-formats code to a consistent style — no debate needed
ruff check your_project/  # lints for style issues, unused imports, common bugs, and more

black is an "opinionated" formatter — it makes formatting decisions for you (line breaks, quote style, spacing) with almost no configuration, specifically to end bikeshedding over formatting preferences within a team. ruff is a fast linter that catches style violations, unused imports, and a range of likely bugs, and has largely replaced older tools like flake8 and pylint in new projects due to its speed. Most real teams wire both into CI, so style consistency is enforced automatically rather than relying on every engineer memorising every rule.

// Part 09 — Real World
💼 What This Looks Like at Work

A Code Review Slowed by Style Debates, at a Kansas City Startup

Scenario — Startup, Kansas City · Engineering process retrospective

A small team notices that a large fraction of their pull request review comments are about formatting — spacing, line length, whether a string should use single or double quotes — rather than actual logic, correctness, or design. Reviewers spend real time on comments that add no value to the software itself, and authors get frustrated re-litigating style preferences that are ultimately arbitrary.

What changed
# Added to CI, blocking merge on failure:
black --check .
ruff check .

# Added to pre-commit hooks, so formatting issues never even reach a PR:
pip install pre-commit
pre-commit install

The actual effect on the team's process

Style-related review comments dropped to nearly zero within two weeks — not because engineers suddenly became more careful, but because black and ruff now caught and auto-fixed nearly everything before a human reviewer ever saw the code. Code review time genuinely shifted toward the things that actually needed a human's judgement — architecture, edge cases, whether the logic was correct — which the team considered the single highest-leverage process change they made that quarter.

// Part 10 — Misconceptions

Four Misconceptions About Python Style

✕ ""PEP 8 is enforced by the Python interpreter""
It is purely a style guide — a document, not a language rule. Code violating every PEP 8 convention still runs perfectly. Enforcement, where it happens, comes from separate tools like black and ruff, typically wired into CI, not from Python itself.
✕ ""Following PEP 8 strictly is always more important than matching an existing codebase's conventions""
PEP 8 itself explicitly notes that project-level consistency can outweigh strict adherence — introducing a "more correct" but different style into one function of an otherwise consistent codebase usually reduces overall readability rather than improving it.
✕ ""A docstring should explain HOW a function works internally""
A docstring documents the CONTRACT — what it expects as input, what it returns, what can go wrong — the code itself already shows how it works internally to anyone who reads the function body; restating that in the docstring is redundant and drifts out of sync as the implementation changes.
✕ ""Using a linter/formatter means you don't need to understand PEP 8 at all""
Automated tools handle mechanical formatting reliably, but naming quality, docstring content, avoiding overly long functions, and genuine code smells still require human judgement — the tools remove the tedious, mechanical part of style, not the design thinking behind good code.
// Part 11 — Interview Prep

5 Interview Questions — With Complete Answers

What is PEP 8, and is it enforced by the Python language itself?
PEP 8 is Python's official style guide, covering naming conventions, layout, whitespace, and more. It is not enforced by the interpreter at all — code that violates it runs identically. Its value is social/practical: shared conventions let any Python developer read unfamiliar code without a mental translation step.
What naming convention does Python use for classes vs functions/variables?
Classes use PascalCase (PaymentProcessor); functions, variables, and methods use snake_case (calculate_total, user_name); constants use SCREAMING_SNAKE_CASE (MAX_RETRIES). Mixing these conventions still runs fine but immediately signals unfamiliarity with Python idioms to an experienced reader.
What should a well-written docstring document, and what should it generally avoid?
A function's contract — what arguments it expects, what it returns, and what exceptions it can raise — not its internal implementation details, which the code body itself already shows and which would drift out of sync as the implementation changes over time.
What is the difference between what a tool like black does versus what ruff does?
black is an opinionated auto-formatter — it rewrites code to a consistent style (spacing, line breaks, quote style) with minimal configuration, ending team debates over formatting preferences. ruff is a fast linter that flags style violations, unused imports, and likely bugs, without necessarily rewriting the code itself.
Does PEP 8 require strict, unconditional adherence in every situation?
No — PEP 8 itself notes that consistency within a project can outweigh strict adherence to the guide. Matching an existing codebase's established conventions, even where they differ slightly from PEP 8's letter, is often more valuable for overall readability than introducing a one-off "more correct" style that breaks consistency with the surrounding code.
// Common Mistakes

Style Mistakes Beginners Make Constantly

Mixing naming conventions within the same codebase
A function named calculateTotal alongside others named calculate_discount reads as inconsistent and unfamiliar to any experienced Python reader — pick snake_case for functions/variables and stay consistent throughout.
Writing a docstring that just restates the function name in sentence form
"""Calculates the total.""" on a function called calculate_total() adds nothing a reader did not already know from the name alone — a useful docstring explains parameters, return value, and possible exceptions, not just a rephrasing of the name.
Manually formatting code instead of running a formatter
Hand-aligning spacing or manually wrapping long lines is time-consuming and inconsistent between engineers — running black (or an equivalent) automatically produces consistent formatting in a fraction of a second, with zero manual effort.
Using unexplained "magic numbers" or "magic strings" scattered through the code
if status == 3: gives no hint what 3 means without checking elsewhere; a named constant (STATUS_SHIPPED = 3) makes the same check self-documenting at the point it is used.
// Error Library

Issues You Will Hit With Style Tooling — And Exactly Why

would reformat your_file.py (black --check reports this and exits non-zero)
Cause: The file does not match the formatting black would apply — common in CI when a "check" mode run is used specifically to block merges on unformatted code.
Fix: Run "black your_file.py" (without --check) locally to apply the formatting, then commit the result.
F401 'os' imported but unused
Cause: A ruff/flake8-style linter detected an import that is never actually referenced anywhere in the file.
Fix: Remove the unused import, or use it if it was meant to be used — leaving unused imports around is itself a minor but real code-quality issue linters exist partly to catch.
E501 line too long (105 > 88 characters)
Cause: A line exceeds the project's configured maximum line length.
Fix: Break the line across multiple lines (parentheses allow implicit line continuation in Python), or adjust the project's configured line-length limit if the team has deliberately chosen a different value than the tool's default.

🎯 Key Takeaways

  • PEP 8 is a style guide, not a language rule — it exists so any Python developer can read unfamiliar code without a mental translation step, not because the interpreter requires it.
  • snake_case for functions/variables/modules, PascalCase for classes, SCREAMING_SNAKE_CASE for constants — mixing conventions signals unfamiliarity even though the code still runs.
  • A docstring documents a function's contract (inputs, output, possible exceptions) — not its internal implementation, which the code body already shows.
  • The Zen of Python (import this) maps onto concrete patterns already covered in this track — explicit imports, flat guard-clause logic, one obvious idiomatic way to do things.
  • PEP 8 explicitly allows project-level consistency to outweigh strict adherence — matching an existing codebase's established style is often more valuable than a technically "more correct" but inconsistent change.
  • Tools like black (formatting) and ruff (linting) automate the mechanical parts of style, freeing code review to focus on logic, design, and correctness — genuinely shown to shift review time toward higher-value feedback.

What comes next

The final module of this track — Python Interview Prep — synthesises everything covered across all 46 modules into a focused set of real interview questions and coding patterns.

Module 46 → Python Interview Prep
Share

Discussion

0

Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.

Continue with GitHub
Loading...