Skip to content
SP StackPractices
beginner

Clean Code Principles — Writing Maintainable Software

A practical guide to clean code: meaningful names, short functions, DRY, SOLID foundations, and habits that make codebases easier to read and maintain.

Topics: design

Clean Code Principles

Introduction

Clean code is code that is easy to understand, easy to change, and easy to test. It is not about being clever — it is about being clear. This guide covers the foundational habits that make a codebase sustainable.

Meaningful Names

Names are the most important form of documentation in code.

Use Intention-Revealing Names

# Bad
x = 10  # what is x?

# Good
days_until_expiration = 10
# Bad
def calc(a, b):
    return a * b

# Good
def calculate_total_price(quantity, unit_price):
    return quantity * unit_price

Avoid Disinformation

# Bad
account_list = {}  # it's a dict, not a list

# Good
accounts_by_id = {}

Use Pronounceable Names

# Bad
gen_ymdhms = datetime.now()

# Good
generation_timestamp = datetime.now()

Pick One Word Per Concept

ConceptPick OneAvoid Mixing
Fetch dataget, fetchDon’t use both
Create objectcreate, make, buildPick one
Insert datainsert, add, appendPick one

Short Functions

Functions should do one thing, do it well, and do it only.

The Single Responsibility Rule

# Bad: one function does validation, calculation, and persistence
def process_order(order):
    if not order.items:
        raise ValueError("Empty order")
    total = sum(item.price * item.qty for item in order.items)
    if order.customer.is_vip:
        total *= 0.9
    db.execute("INSERT INTO orders ...", total)
    send_email(order.customer.email, f"Order {total} confirmed")

# Good: compose small functions
def validate_order(order):
    if not order.items:
        raise ValueError("Empty order")

def calculate_total(order):
    total = sum(item.price * item.qty for item in order.items)
    return apply_vip_discount(total, order.customer)

def apply_vip_discount(total, customer):
    return total * 0.9 if customer.is_vip else total

def save_order(order, total):
    db.execute("INSERT INTO orders ...", total)

def confirm_order(order, total):
    validate_order(order)
    total = calculate_total(order)
    save_order(order, total)
    send_email(order.customer.email, f"Order {total} confirmed")

Keep Functions Short

Aim for 20 lines or fewer. If a function exceeds this, it is likely doing more than one thing.

Minimize Parameters

Number of ArgsReadability
0-1Ideal
2Reasonable
3Suspicious
>3Requires justification (use a struct/object)

DRY — Don’t Repeat Yourself

Duplication is the root of maintenance pain. When logic is repeated, a bug fix in one place often misses the others.

# Bad: repeated validation logic
def create_user(email, password):
    if "@" not in email:
        raise ValueError("Invalid email")
    ...

def update_user_email(user_id, email):
    if "@" not in email:
        raise ValueError("Invalid email")
    ...

# Good: extract shared logic
validate_email(email):
    if "@" not in email:
        raise ValueError("Invalid email")

def create_user(email, password):
    validate_email(email)
    ...

def update_user_email(user_id, email):
    validate_email(email)
    ...

Comments

Comments should explain why, not what. The code itself should explain the what.

# Bad: comment restates the obvious
count = count + 1  # increment count

# Bad: comment explains what the code does
# Check if user is active and has permission
if user.is_active and user.has_permission("read"):
    ...

# Good: comment explains why
# Skip inactive users because they may have stale permissions
# after an offboarding delay (see policy HR-2024-03)
if user.is_active and user.has_permission("read"):
    ...

Prefer Self-Documenting Code

# Bad
# returns 1 if the user can access the resource
if check(u, r) == 1:
    ...

# Good
if user.can_access(resource):
    ...

Error Handling

Errors are part of the domain, not an afterthought.

Use Exceptions, Not Return Codes

# Bad
def read_file(path):
    if not os.path.exists(path):
        return None  # caller must check for None
    return open(path).read()

result = read_file("config.txt")
if result is None:
    ...  # error handling scattered

# Good
def read_file(path):
    if not os.path.exists(path):
        raise FileNotFoundError(f"{path} not found")
    return open(path).read()

try:
    content = read_file("config.txt")
except FileNotFoundError as e:
    logger.error(e)
    ...

Don’t Swallow Exceptions

# Bad
try:
    risky_operation()
except Exception:
    pass  # silent failure

# Good
try:
    risky_operation()
except NetworkError as e:
    logger.warning("Network issue, will retry", exc_info=e)
    retry()

Formatting

Consistency matters more than the specific style. Pick a standard, automate it, and move on.

  • Use a linter/formatter (Prettier, Black, gofmt)
  • Keep related code vertically close — declaration and usage should be near each other
  • Limit line length — 80-100 characters is a readable range
  • Use blank lines to separate logical groups

Objects and Data Structures

Tell, Don’t Ask

# Bad: asking about state, then deciding
if account.status == "overdrawn":
    account.lock()

# Good: tell the object what to do
account.check_overdrawn_and_lock()

The Law of Demeter

A method should only call:

  1. Methods on itself
  2. Methods on parameters
  3. Methods on objects it creates
  4. Methods on direct components (fields)
# Bad: navigating deep into an object graph
customer.orders[-1].items[0].price

# Good: encapsulate the navigation
customer.last_order_first_item_price()

Best Practices

  • Leave the code cleaner than you found it (Boy Scout Rule)
  • Delete dead code — commented-out code, unused functions, unreachable branches
  • Write tests first — they force you to write testable (hence clean) code
  • Code is read 10x more than it is written — optimize for the reader
  • Pair programming — two eyes catch complexity before it compounds

Common Mistakes

  • Optimizing for brevity instead of clarity
  • Using abbreviations that only the author understands
  • Functions with side effects that surprise the caller
  • Magic numbers and strings scattered throughout the code
  • Comments that drift out of sync with the code they describe
  • Deep nesting (“arrow code”) that obscures the happy path

Frequently Asked Questions

Q: Should I refactor legacy code that isn’t broken? A: Follow the Boy Scout Rule: clean up the parts you touch. Don’t embark on large rewrites without business justification and test coverage.

Q: How do I convince my team to adopt clean code practices? A: Start with automated formatting (zero debate), then introduce code review checklists. Show concrete examples of bugs caused by unclear code.

Q: Is clean code slower to write? A: Slightly slower to write, significantly faster to read, debug, and change. The investment pays off within the first modification.