Code is read far more often than it's written. Here's how to make your Python code maintainable.
Naming Things Well
Names should reveal intent:
# Bad
d = 86400
lst = get_data()
def proc(x):
pass
# Good
SECONDS_PER_DAY = 86400
active_users = get_active_users()
def process_payment(transaction):
passGuidelines:
- Variables: nouns that describe what they hold
- Functions: verbs that describe what they do
- Booleans:
is_,has_,can_prefixes - Constants:
UPPER_SNAKE_CASE
Function Design
Functions should do one thing:
# Bad - does too much
def handle_user(user_data):
validated = validate(user_data)
user = create_user(validated)
send_welcome_email(user)
log_signup(user)
return user
# Better - single responsibility
def create_user(user_data):
validated = validate(user_data)
return User(**validated)
# Called separately
user = create_user(data)
send_welcome_email(user)
log_signup(user)Keep functions short. If you need comments to explain sections, those sections might be separate functions.
Default Arguments
Use sensible defaults:
def fetch_users(
limit: int = 100,
include_inactive: bool = False,
sort_by: str = "created_at"
) -> list[User]:
pass
# Caller only specifies what differs
users = fetch_users(limit=50)Never use mutable defaults:
# Wrong - shared list across calls
def add_item(item, items=[]):
items.append(item)
return items
# Right
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return itemsEarly Returns
Reduce nesting with guard clauses:
# Hard to follow
def process_order(order):
if order:
if order.is_valid:
if order.items:
# actual logic buried here
return calculate_total(order)
else:
return 0
else:
raise InvalidOrder()
else:
raise OrderNotFound()
# Clearer with early returns
def process_order(order):
if not order:
raise OrderNotFound()
if not order.is_valid:
raise InvalidOrder()
if not order.items:
return 0
return calculate_total(order)Meaningful Structure
Group related code:
class OrderProcessor:
# Public interface first
def process(self, order: Order) -> Receipt:
self._validate(order)
total = self._calculate_total(order)
return self._create_receipt(order, total)
# Private helpers below
def _validate(self, order: Order) -> None:
...
def _calculate_total(self, order: Order) -> Decimal:
...
def _create_receipt(self, order: Order, total: Decimal) -> Receipt:
...Comments That Add Value
Don't explain what—explain why:
# Bad - restates the code
# Increment counter by 1
counter += 1
# Good - explains the why
# Rate limit: max 100 requests per minute per user
if request_count > 100:
raise RateLimitExceeded()
# Good - documents non-obvious behavior
# Sleep briefly to avoid hammering the API during retry
time.sleep(0.1 * attempt)Use Type Hints
Types serve as documentation and catch bugs:
from typing import Optional
from datetime import datetime
def find_user(
user_id: int,
include_deleted: bool = False
) -> Optional[User]:
"""Find a user by ID.
Returns None if not found.
"""
...Error Handling
Be specific about exceptions:
# Bad - catches everything
try:
result = risky_operation()
except:
pass
# Better - specific exceptions
try:
result = fetch_from_api()
except requests.Timeout:
logger.warning("API timeout, using cached data")
result = get_cached_data()
except requests.HTTPError as e:
logger.error(f"API error: {e}")
raiseConfiguration
Separate config from code:
# config.py
from pydantic import BaseSettings
class Settings(BaseSettings):
database_url: str
api_key: str
debug: bool = False
class Config:
env_file = ".env"
settings = Settings()
# usage.py
from config import settings
db = connect(settings.database_url)Testing Considerations
Write code that's easy to test:
# Hard to test - hidden dependency
def send_notification(user_id):
user = database.get_user(user_id) # Global database
email.send(user.email) # Global email service
# Easy to test - dependencies injected
def send_notification(user: User, email_service: EmailService):
email_service.send(user.email)Module Organization
Keep files focused:
myapp/
├── __init__.py
├── models.py # Data structures
├── services.py # Business logic
├── api.py # HTTP handlers
├── database.py # Data access
└── utils.py # Shared helpers
When a file grows too large, split by feature:
myapp/
├── users/
│ ├── models.py
│ ├── services.py
│ └── api.py
├── orders/
│ ├── models.py
│ ├── services.py
│ └── api.py
└── shared/
└── utils.py
The Readability Test
Ask yourself:
- Can someone new understand this in 5 minutes?
- Will I understand this in 6 months?
- Is the purpose clear without running the code?
If not, refactor until it is. Your future self will thank you.