Clean code is not about making code pretty - it’s about making code understandable, maintainable, and extensible. This guide covers principles and techniques for writing code that stands the test of time.
Meaningful Names
The name should reveal intent. If you need a comment to explain a variable name, the name is wrong.
Bad vs Good
# BAD - What does d represent?
d = 23
for i in range(t):
x[i] = x[i] - m * y[i]
# GOOD - Intent is clear
discount_rate = 0.23
number_of_transactions = total_transactions
for index in range(number_of_transactions):
final_prices[index] = gross_prices[index] - discount_rate * cost_basis[index]
Naming Conventions
# Variables - noun, descriptive
user = User()
order_items = []
total_price = calculate_total()
# Functions - verb, describe action
def calculate_total():
...
def fetch_user_by_id():
...
def send_order_confirmation():
...
# Classes - noun, represents thing
class OrderProcessor:
...
class PaymentGateway:
...
class InventoryManager:
...
# Constants - all caps with underscores
MAX_RETRY_ATTEMPTS = 3
DEFAULT_TIMEOUT_SECONDS = 30
API_BASE_URL = "https://api.example.com"
# Boolean - prefix with is, has, should, can
is_active = True
has_permission = False
should_retry = True
can_proceed = True
Avoid Magic Numbers
# BAD
if age > 18:
...
# GOOD
AGE_OF_MAJORITY = 18
if age > AGE_OF_MAJORITY:
...
Functions
Single Responsibility
Each function should do one thing well.
# BAD - Multiple responsibilities
def save_user_and_send_email(user):
# Validate
if not user.email:
raise ValueError("Email required")
# Save to database
db.save(user)
# Send email
email_service.send(user.email, "Welcome!")
# Log
logger.info(f"User {user.id} saved")
# GOOD - Separated concerns
def validate_user(user):
if not user.email:
raise ValueError("Email required")
return user
def save_user_to_database(user):
return db.save(user)
def notify_user_registration(user):
email_service.send(user.email, "Welcome!")
def log_user_registration(user):
logger.info(f"User {user.id} saved")
def register_user(user):
validate_user(user)
save_user_to_database(user)
notify_user_registration(user)
log_user_registration(user)
Small Functions
Prefer small, focused functions over large ones.
# BAD - Long function
def process_order(order):
# Validate order
if not order.items:
raise ValueError("Empty order")
# Calculate total
total = 0
for item in order.items:
total += item.price * item.quantity
# Apply discount
if order.customer.tier == "premium":
total *= 0.9
# Check inventory
for item in order.items:
available = inventory.get(item.product_id)
if available < item.quantity:
raise ValueError(f"Insufficient inventory for {item.product_id}")
# Reserve inventory
for item in order.items:
inventory.reserve(item.product_id, item.quantity)
# Process payment
payment.charge(order.customer, total)
# Create shipment
shipment = shipping.create(order)
# Send notifications
email.send(order.customer, "Order confirmed")
sms.send(order.customer, "Order confirmed")
return {"order_id": order.id, "total": total}
# GOOD - Composed of small functions
def process_order(order):
validate_order(order)
total = calculate_order_total(order)
total = apply_discount(total, order.customer)
reserve_inventory(order.items)
charge_payment(order.customer, total)
shipment = create_shipment(order)
notify_customer(order)
return {"order_id": order.id, "total": total}
def calculate_order_total(order):
return sum(item.price * item.quantity for item in order.items)
def apply_discount(total, customer):
if customer.tier == "premium":
return total * 0.9
return total
Parameters
# BAD - Too many parameters
def create_user(name, email, age, address, phone, role, verified, newsletter):
...
# GOOD - Use objects
def create_user(user_data):
...
class CreateUserDTO:
def __init__(self, name, email, age, address, phone, role, verified, newsletter):
...
# Or use builder pattern
user = (UserBuilder()
.with_name("John")
.with_email("[email protected]")
.with_role("admin")
.build())
SOLID Principles
Single Responsibility
A class should have only one reason to change.
# BAD - Multiple responsibilities
class User:
def __init__(self, name, email):
self.name = name
self.email = email
def save(self):
# Database logic
db.save(self)
def validate(self):
# Validation logic
if not self.email:
raise ValueError("Email required")
def send_email(self):
# Email logic
email_service.send(self.email, "Hello")
# GOOD - Separated
class User:
def __init__(self, name, email):
self.name = name
self.email = email
class UserValidator:
def validate(self, user):
if not user.email:
raise ValueError("Email required")
class UserRepository:
def save(self, user):
db.save(user)
class UserNotifier:
def notify(self, user):
email_service.send(user.email, "Hello")
Open/Closed
Open for extension, closed for modification.
# BAD - Must modify to add new discount types
def calculate_discount(order):
if order.discount_type == "percentage":
return order.total * order.discount_value / 100
elif order.discount_type == "fixed":
return order.discount_value
elif order.discount_type == "shipping":
return order.shipping_cost
# Have to modify this function for new types!
# GOOD - Extend via new classes
class Discount(ABC):
@abstractmethod
def apply(self, order) -> Decimal:
...
class PercentageDiscount(Discount):
def apply(self, order) -> Decimal:
return order.total * order.discount_value / 100
class FixedDiscount(Discount):
def apply(self, order) -> Decimal:
return order.discount_value
class ShippingDiscount(Discount):
def apply(self, order) -> Decimal:
return order.shipping_cost
Liskov Substitution
Subtypes must be substitutable for their base types.
# BAD - Subclass changes behavior unexpectedly
class Bird:
def fly(self):
return "Flying"
class Penguin(Bird):
def fly(self):
raise NotImplementedError("Penguins can't fly")
def make_bird_fly(bird: Bird):
return bird.fly() # Crashes with penguin!
# GOOD - Proper abstraction
class Bird(ABC):
@abstractmethod
def move(self):
...
class FlyingBird(Bird):
def move(self):
return "Flying"
class WalkingBird(Bird):
def move(self):
return "Walking"
def make_bird_move(bird: Bird):
return bird.move() # Works with both
Interface Segregation
Prefer small, focused interfaces over large ones.
# BAD - Large interface
class Machine(ABC):
@abstractmethod
def print(self): ...
@abstractmethod
def scan(self): ...
@abstractmethod
def fax(self): ...
class OldPrinter(Machine):
def print(self): ...
def scan(self): raise NotImplementedError() # Must implement
def fax(self): raise NotImplementedError()
# GOOD - Small interfaces
class Printer(ABC):
@abstractmethod
def print(self): ...
class Scanner(ABC):
@abstractmethod
def scan(self): ...
class Fax(ABC):
@abstractmethod
def fax(self): ...
class OldPrinter(Printer):
def print(self): ...
Dependency Inversion
Depend on abstractions, not concretions.
# BAD - Direct dependency on concrete class
class OrderService:
def __init__(self):
self.email_service = SendGridEmailService()
def send_confirmation(self, order):
self.email_service.send(order.customer.email, "Confirmed!")
# GOOD - Depend on abstraction
class EmailService(ABC):
@abstractmethod
def send(self, to, message): ...
class OrderService:
def __init__(self, email_service: EmailService):
self.email_service = email_service
def send_confirmation(self, order):
self.email_service.send(order.customer.email, "Confirmed!")
# Can inject any email service
order_service = OrderService(SendGridEmailService())
order_service = OrderService(AWSEmailService())
order_service = OrderService(ConsoleEmailService())
Code Formatting
Consistent Style
# Use formatter (Black, Prettier) - don't waste energy debating
# Import organization
import os
import sys
import third_party
import another_third_party
from package import local_module
from package.local import something
# Line length - typically 79-100 characters
# Use line continuation for long lines
def very_long_function_name(
argument_one,
argument_two,
argument_three
):
return argument_one + argument_two + argument_three
# Whitespace
x = 1 # Spaces around operators
items = [1, 2, 3] # Spaces after commas
result = func(arg1, arg2) # No spaces inside parens
Error Handling
Use Exceptions
# BAD - Using return codes
def find_user(user_id):
user = db.query("SELECT * FROM users WHERE id = ?", user_id)
if user:
return user
return None # What does None mean?
def get_user_name(user_id):
user = find_user(user_id)
if user:
return user.name
return "" # Is empty string better or worse than None?
# GOOD - Use exceptions
def find_user(user_id):
user = db.query("SELECT * FROM users WHERE id = ?", user_id)
if not user:
raise UserNotFoundError(f"User {user_id} not found")
return user
def get_user_name(user_id):
user = find_user(user_id)
return user.name
Fail Fast
# BAD - Check at the end
def process_order(order):
result = do_first_step(order)
result = do_second_step(result)
result = do_third_step(result)
# Error discovered too late
if not order.customer.is_valid:
raise ValueError("Invalid customer")
return result
# GOOD - Validate early
def process_order(order):
validate_order(order) # Fail fast
result = do_first_step(order)
result = do_second_step(result)
result = do_third_step(result)
return result
Comments
When to Use Comments
Comments should explain “why”, not “what”.
# BAD - Explains what code does (code should do this)
# Increment counter by 1
counter += 1
# Check if user is active
if user.is_active:
...
# GOOD - Explains why
# Rate limit exceeded - retry after cooldown to avoid IP ban
if requests_this_minute > MAX_REQUESTS:
await asyncio.sleep(COOLDOWN_SECONDS)
# Using exponential backoff because external API has rate limits
# and aggressive retries could trigger temporary bans
for attempt in range(MAX_RETRIES):
try:
return api.call()
except RateLimited:
await asyncio.sleep(2 ** attempt)
Testing
Clean code is testable code.
# BAD - Hard to test - database, email, logger all mixed
def register_user(name, email):
if not email:
logger.error("No email provided")
return False
db.execute("INSERT INTO users VALUES (?, ?)", name, email)
email_service.send(email, "Welcome!")
logger.info(f"User {name} registered")
return True
# GOOD - Easy to test
def register_user(user, repository, notifier, logger):
user.validate()
repository.save(user)
notifier.send_welcome(user)
logger.info(f"User {user.name} registered")
# Test becomes simple
def test_register_user_saves_to_repository():
mock_repo = Mock()
mock_notifier = Mock()
mock_logger = Mock()
user = User("John", "[email protected]")
register_user(user, mock_repo, mock_notifier, mock_logger)
mock_repo.save.assert_called_once_with(user)
mock_notifier.send_welcome.assert_called_once_with(user)
Refactoring Checklist
Before submitting code, verify:
- Meaningful variable/function names
- Functions do one thing
- No magic numbers or strings
- Error handling in place
- No deeply nested code
- Duplicated code extracted
- Large functions split
- SOLID principles followed
- Code is testable
- Comments explain “why”
Conclusion
Clean code is a habit, not a destination:
- Write code for humans first, computers second
- Refactor continuously, not in big batches
- Apply SOLID principles judiciously
- Testability is a design feature
- Your future self will thank you
External Resources
Related Articles
- Refactoring Strategies - Safe refactoring techniques
- Code Quality Metrics - Measuring code quality
- Code Review Best Practices - Enforcing standards
Comments