PEP 8 - Python Style Guide
Table of Contents
- What is PEP 8?
- Naming Conventions
- Code Layout
- Whitespace and Formatting
- Comments and Docstrings
- Best Practices
- Common Pitfalls
What is PEP 8?
PEP 8 (Python Enhancement Proposal 8) is the official style guide for Python code.
Purpose: - 📖 Improve code readability - 🔄 Ensure consistency across projects - 🛠️ Enhance maintainability
Key Principle: "Code is read much more often than it is written"
Naming Conventions
Overview Table
| Element | Convention | Example |
|---|---|---|
| Variables | snake_case |
user_name, total_count |
| Functions | snake_case |
get_data(), calculate_total() |
| Classes | PascalCase |
UserProfile, DatabaseConnection |
| Constants | UPPER_SNAKE_CASE |
MAX_SIZE, API_KEY |
| Modules | lowercase |
utils, mymodule |
| Packages | lowercase |
numpy, requests |
| Private | _leading_underscore |
_internal_var, _helper() |
| Protected | _single_underscore |
_internal_method |
| Name Mangling | __double_underscore |
__private_var |
Code Layout
Indentation
Always use 4 spaces per indentation level.
# Good - 4 spaces
def my_function():
if condition:
do_something()
do_another_thing()
return result
# Bad - 2 spaces (not PEP 8)
def my_function():
if condition:
do_something()
return result
# Bad - tabs (never use tabs)
def my_function():
if condition:
do_something()
Line Length
Maximum line lengths: - 79 characters for code - 72 characters for comments and docstrings
Breaking lines:
# Good - implicit line joining (preferred)
total = (first_variable + second_variable
+ third_variable + fourth_variable)
# Good - backslash continuation (use sparingly)
total = first_variable + second_variable \
+ third_variable + fourth_variable
# Good - function arguments
result = my_long_function_name(
argument1, argument2,
argument3, argument4
)
Blank Lines
Use blank lines to organize code logically.
# 2 blank lines before top-level functions and classes
import os
import sys
def top_level_function():
"""Top-level function."""
pass
class MyClass:
"""My class."""
# 1 blank line between methods
def __init__(self):
"""Initialize."""
self.value = 0
def method_one(self):
"""First method."""
pass
def method_two(self):
"""Second method."""
pass
class AnotherClass:
"""Another class."""
pass
Inside functions:
def process_data(data):
"""Process data with logical grouping."""
# Group 1: Input validation
if not data:
return None
# Group 2: Data processing
cleaned_data = clean(data)
processed_data = transform(cleaned_data)
# Group 3: Return result
return processed_data
Import Order
Organize imports in three groups with blank lines between:
# 1. Standard library imports
import os
import sys
from datetime import datetime
# 2. Third-party imports
import numpy as np
import requests
from flask import Flask, render_template
# 3. Local application imports
from myapp.utils import helper_function
from myapp.models import User, Database
from myapp.config import API_KEY
Import Guidelines:
# Good - one import per line (standard library)
import os
import sys
import json
# Good - multiple items from same module
from datetime import datetime, timedelta, timezone
# Good - aliases (common conventions)
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# Avoid - wildcard imports
from module import * # Bad - unclear what's imported
# Good - explicit imports
from module import function1, function2, Class1
Whitespace and Formatting
Spaces Around Operators
# Good - spaces around operators
x = 1 + 2
y = x * 3 - 4
result = (x + y) / 2
# Good - no space for high-priority operators
hypot = x*x + y*y
c = (a + b) * (a - b)
# Good - function arguments
def function(arg1, arg2, arg3):
pass
# Good - function calls
result = function(1, 2, 3)
# Bad - inconsistent spacing
x=1+2
y = x*3-4
result=function( 1,2 ,3 )
No Spaces Inside Brackets
# Good
spam(ham[1], {eggs: 2})
dict['key'] = list[index]
# Bad
spam( ham[ 1 ], { eggs: 2 } )
dict ['key'] = list [index]
No Trailing Whitespace
# Good
x = 1
y = 2
# Bad (spaces after 1 and 2)
x = 1
y = 2
Comments and Docstrings
Inline Comments
# Good - explain why, not what
x = x + 1 # Compensate for border
# Bad - states the obvious
x = x + 1 # Increment x
# Good - useful context
if i & (i - 1) == 0: # Check if i is power of 2
pass
Guidelines:
- Use sparingly
- Start with # followed by single space
- Should be on separate line when possible
- Explain why, not what
Block Comments
# Good - explain complex logic
# Calculate the trajectory using the following formula:
# trajectory = initial_velocity * time + 0.5 * acceleration * time^2
# This accounts for both initial velocity and constant acceleration
trajectory = v0 * t + 0.5 * a * t**2
Docstrings
Module docstring:
"""
mymodule - Utility functions for data processing.
This module provides functions for cleaning, transforming,
and validating data from various sources.
"""
Function docstring:
def add_numbers(a, b):
"""
Add two numbers and return the result.
Args:
a (int or float): First number
b (int or float): Second number
Returns:
int or float: Sum of a and b
Raises:
TypeError: If arguments are not numeric
Example:
>>> add_numbers(2, 3)
5
"""
return a + b
Class docstring:
class BankAccount:
"""
A bank account with basic operations.
Attributes:
balance (float): Current account balance
owner (str): Account owner name
Methods:
deposit(amount): Add money to account
withdraw(amount): Remove money from account
"""
def __init__(self, owner, balance=0):
"""
Initialize bank account.
Args:
owner (str): Account owner name
balance (float, optional): Initial balance. Defaults to 0.
"""
self.owner = owner
self.balance = balance
Best Practices
String Formatting
name = "Alice"
age = 30
score = 95.5
# Best - f-strings (Python 3.6+)
message = f"Hello {name}, you are {age} years old"
formatted = f"Score: {score:.2f}" # 2 decimal places
# Good - .format() method
message = "Hello {}, you are {} years old".format(name, age)
message = "Hello {name}, you are {age} years old".format(name=name, age=age)
# Avoid - % formatting (outdated)
message = "Hello %s, you are %d years old" % (name, age)
# Avoid - string concatenation
message = "Hello " + name + ", you are " + str(age) + " years old"
Comparisons
# Good - use 'is' for None, True, False
if value is None:
pass
if flag is True: # Or just: if flag:
pass
if result is not None:
pass
# Bad - don't use == for None, True, False
if value == None: # Wrong!
pass
# Good - implicit boolean check
if items: # Check if list is not empty
pass
if not items: # Check if list is empty
pass
# Bad - explicit comparison
if len(items) > 0: # Unnecessary
pass
List/Dict Comprehensions
# Good - simple comprehension
squares = [x**2 for x in range(10)]
even_squares = [x**2 for x in range(10) if x % 2 == 0]
# Good - dict comprehension
word_lengths = {word: len(word) for word in words}
# Bad - too complex (use regular loop instead)
result = [x**2 if x % 2 == 0 else x**3 for x in range(100)
if x > 10 and x < 90 and x % 3 == 0]
# Better - use regular loop for complex logic
result = []
for x in range(10, 90):
if x % 3 == 0:
if x % 2 == 0:
result.append(x**2)
else:
result.append(x**3)
Context Managers (with statement)
# Good - file automatically closes
with open('data.txt', 'r') as f:
data = f.read()
# Bad - file might not close on error
f = open('data.txt', 'r')
data = f.read()
f.close()
# Good - multiple context managers
with open('input.txt', 'r') as infile, \
open('output.txt', 'w') as outfile:
data = infile.read()
outfile.write(data.upper())
Common Pitfalls
None Comparison
# Good - use 'is' for None
if value is None:
pass
if value is not None:
pass
# Bad - don't use ==
if value == None: # Wrong!
pass
# Bad - don't use negation
if not value is None: # Confusing!
pass
Default Mutable Arguments
# Bad - mutable default argument (creates shared list!)
def append_to_list(item, lst=[]):
lst.append(item)
return lst
# Good - use None as default
def append_to_list(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst
Exception Handling
# Good - specific exceptions
try:
result = risky_operation()
except ValueError as e:
print(f"Invalid value: {e}")
except FileNotFoundError as e:
print(f"File not found: {e}")
# Bad - bare except (catches everything, including KeyboardInterrupt!)
try:
result = risky_operation()
except: # Too broad!
print("Something went wrong")
# Good - catch all exceptions explicitly
try:
result = risky_operation()
except Exception as e: # Explicit but still broad
print(f"Error: {e}")
Lambda Functions
# Good - simple, one-line operations
squares = map(lambda x: x**2, numbers)
sorted_items = sorted(items, key=lambda x: x.name)
# Bad - complex lambda (use regular function)
result = map(lambda x: x**2 if x % 2 == 0 else x**3 if x > 10 else x, numbers)
# Better - use regular function for complex logic
def transform(x):
if x % 2 == 0:
return x**2
elif x > 10:
return x**3
else:
return x
result = map(transform, numbers)
Additional Resources
- PEP 8 Official: peps.python.org/pep-0008
- PEP 8 Online Checker: pep8online.com
- Black Documentation: black.readthedocs.io
- Flake8 Documentation: flake8.pycqa.org
- Real Python PEP 8 Guide: realpython.com/python-pep8