Skip to main content
โšก Calmops

Clean Code Principles: Writing Maintainable Software

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

Comments