Python Best Practices — Writing Clean Code
Writing clean, maintainable code is a skill that separates junior from senior developers. Follow these practices to write professional Python code.
Learning Objectives
- Follow PEP 8 style guidelines consistently
- Write Pythonic idioms that leverage language features
- Structure projects for maintainability and scalability
- Handle errors gracefully with proper exception hierarchy
PEP 8 Style Guide
The foundation of Python code style:
# Naming conventions
class UserManager: # CamelCase for classes
MAX_RETRIES = 3 # UPPER_CASE for constants
def get_user(self): # snake_case for functions/methods
user_name = "" # snake_case for variables
# Line length: 79 characters max (72 for docstrings)
# Imports: one per line, grouped (stdlib, third-party, local)
import os
import sys
from pathlib import Path
from flask import Flask
from sqlalchemy import Column
from myapp.models import User
# Whitespace matters
x = 1
y = x + 1
d = {"key": "value"}
lst = [1, 2, 3]
Pythonic Idioms
Write idiomatic Python that leverages the language:
# Use enumerate instead of manual index
for i, item in enumerate(items):
print(i, item)
# Use zip for parallel iteration
for name, score in zip(names, scores):
print(f"{name}: {score}")
# Use any/all for boolean checks
if any(x > 10 for x in numbers):
print("Found large number")
# Use context managers (always!)
with open("file.txt") as f:
data = f.read()
# Use f-strings (Python 3.6+)
message = f"Hello, {name}!"
# EAFP — Easier to Ask Forgiveness than Permission
try:
value = my_dict["key"]
except KeyError:
value = default
# Use unpacking
first, *middle, last = [1, 2, 3, 4, 5]
# first=1, middle=[2,3,4], last=5
SOLID Principles
Apply these object-oriented design principles:
| Principle | Meaning | Example |
|---|---|---|
| Single Responsibility | One class = one job | User class handles users, not email sending |
| Open/Closed | Open for extension, closed for modification | Use abstract base classes |
| Liskov Substitution | Subtypes must be substitutable | Dog subclass works where Animal expected |
| Interface Segregation | Small, specific interfaces | Separate Readable and Writable |
| Dependency Inversion | Depend on abstractions | Use ABC, not concrete classes |
Project Structure
Organize code for maintainability:
Architecture Diagram
my_project/
+-- src/
| +-- my_package/
| +-- __init__.py
| +-- models/
| | +-- __init__.py
| | +-- user.py
| +-- services/
| | +-- __init__.py
| | +-- auth.py
| +-- utils/
| +-- __init__.py
| +-- helpers.py
+-- tests/
| +-- __init__.py
| +-- test_models/
| +-- test_services/
+-- pyproject.toml
+-- README.md
+-- .gitignore
Error Handling Best Practices
Be specific and informative:
# Be specific with exceptions
try:
result = risky_operation()
except ValueError as e:
logger.error(f"Value error: {e}")
raise
except ConnectionError:
logger.error("Connection failed")
return fallback_value
# Create custom exceptions for your domain
class InsufficientFundsError(Exception):
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(
f"Insufficient funds: balance={balance}, requested={amount}"
)
# Don't catch all exceptions — be specific
# BAD: except Exception:
# GOOD: except (ValueError, TypeError) as e:
Documentation Standards
Write clear, useful documentation:
def calculate_discount(price: float, discount: float) -> float:
"""Calculate discounted price.
Args:
price: Original price (must be positive)
discount: Discount percentage (0.0 to 1.0)
Returns:
Price after discount
Raises:
ValueError: If discount is not between 0 and 1
Example:
>>> calculate_discount(100.0, 0.2)
80.0
"""
if not 0 <= discount <= 1:
raise ValueError(f"Discount must be 0-1, got {discount}")
return price * (1 - discount)
Code Review Checklist
Before submitting code, verify:
- Functions do one thing (Single Responsibility)
- Names are descriptive (no
x,tmp,data) - No duplicated code (DRY principle)
- Error handling is specific (not bare
except:) - Public functions have docstrings
- Tests cover edge cases
- No hardcoded values (use constants or config)
- Imports are organized (stdlib -> third-party -> local)
Common Mistakes
| Mistake | Problem | Better Approach |
|---|---|---|
| Mutable default args | def f(x=[]) | def f(x=None) then x = x or [] |
| Bare except | Catches everything | Catch specific exceptions |
| Global state | Hard to test and debug | Pass dependencies explicitly |
| Over-abstraction | Unnecessary complexity | Keep it simple (YAGNI) |
| Premature optimization | Optimize before profiling | Profile first, optimize second |
Key Takeaways
- Follow PEP 8 for consistent style — use
blackformatter to automate - Use Pythonic idioms: enumerate, zip, context managers, f-strings, EAFP
- Be specific with exception handling — never use bare
except: - Write docstrings for all public functions — Google or NumPy style
- Keep functions small and focused — one function = one job
- Use type hints to catch bugs early and improve IDE support