Clean Code Principles Cheat Sheet
Naming conventions, function design, and the SOLID principles for writing readable, maintainable code, illustrated with before/after examples.
Naming Conventions
Rules of thumb for choosing clear, honest variable and function names.
- Intention-revealing names- elapsedTimeInDays is clearer than d; a name should answer why it exists and what it does
- Avoid disinformation- Don't name a variable accountList if it isn't actually a List; don't reuse names with subtly different meanings
- Make names searchable- Single-letter names and magic numbers are hard to grep for; prefer MAX_RETRIES over the literal 3
- Use pronounceable names- genymdhms is hard to discuss out loud; generationTimestamp is not
- Avoid encodings- Modern typed languages and IDEs make Hungarian-notation prefixes like strName or m_ redundant noise
- Nouns for classes, verbs for methods- Customer, InvoiceParser vs. calculateTotal(), sendEmail()
- One word per concept- Pick fetch, get, or retrieve and use it consistently across the codebase, not all three
Before / After: Naming
The same logic rewritten with intention-revealing names and constants.
# Before: unclear names, magic numbers, unclear intentdef calc(x, y): if x > 18: return y * 0.1 return 0# After: intention-revealing names and a named constantADULT_AGE = 18SENIOR_DISCOUNT_RATE = 0.1def calculate_senior_discount(age, price): if age > ADULT_AGE: return price * SENIOR_DISCOUNT_RATE return 0
Small, Focused Functions
Splitting a function that does too much into single-purpose steps.
# Before: one function doing validation, math, and side effectsdef process_order(order): if not order.items: raise ValueError("empty order") total = sum(i.price * i.qty for i in order.items) total *= 0.9 if order.customer.is_member else 1.0 send_email(order.customer.email, f"Total: {total}") save_to_db(order, total) return total# After: each function operates at one level of abstractiondef process_order(order): validate_order(order) total = calculate_total(order) notify_customer(order.customer, total) persist_order(order, total) return total
SOLID Principles
Five object-oriented design principles for maintainable class hierarchies.
- S — Single Responsibility- A class or module should have exactly one reason to change
- O — Open/Closed- Software entities should be open for extension but closed for modification
- L — Liskov Substitution- Subtypes must be substitutable for their base types without breaking correctness
- I — Interface Segregation- Clients shouldn't be forced to depend on methods they don't use; prefer several small interfaces
- D — Dependency Inversion- Depend on abstractions, not concrete implementations; high-level modules shouldn't depend on low-level details
Code Smell Catalog
Recognizable symptoms in code that signal a deeper design problem worth refactoring.
- Long Method- A function that has grown past a screenful and mixes multiple levels of abstraction; extract sub-steps into named helpers
- Large Class / God Object- A class that knows or does too much; split along its distinct responsibilities (Single Responsibility Principle)
- Feature Envy- A method that uses another object's data more than its own; consider moving the method onto that object instead
- Shotgun Surgery- A single change forces edits across many unrelated classes; consolidate the scattered logic into one place
- Primitive Obsession- Using raw strings/ints for concepts like Money or EmailAddress instead of small value objects that enforce invariants
- Data Clumps- The same group of parameters (e.g. street, city, zip) keeps traveling together; bundle them into one object
- Speculative Generality- Abstractions, hooks, or parameters added for a future need that never arrives; delete unused flexibility (YAGNI)
- Divergent Change- One class is edited for many different, unrelated reasons over time; a sign its responsibilities need separating
Guard Clauses over Nested Conditionals
Flattening deeply nested if/else with early returns to keep the happy path unindented.
# Before: arrow-shaped nesting, happy path buried three levels deepdef get_shipping_cost(order): if order is not None: if order.items: if order.destination: if order.destination.is_supported: return calculate_cost(order) else: raise ValueError("unsupported destination") else: raise ValueError("missing destination") else: raise ValueError("empty order") else: raise ValueError("order required")# After: guard clauses fail fast, happy path reads top to bottomdef get_shipping_cost(order): if order is None: raise ValueError("order required") if not order.items: raise ValueError("empty order") if not order.destination: raise ValueError("missing destination") if not order.destination.is_supported: raise ValueError("unsupported destination") return calculate_cost(order)
Command-Query Separation
Splitting a method that both mutates state and returns a value into two single-purpose methods.
# Before: pop() is both a query (returns the top item) and a command (mutates the stack)# This is fine for well-known APIs, but new code should avoid it — it hides side effects.class DirtyStack: def __init__(self): self._items = [] def push_and_get_size(self, item): self._items.append(item) return len(self._items) # command that also returns a query result# After: separate the question from the actionclass CleanStack: def __init__(self): self._items = [] def push(self, item) -> None: # command: changes state, returns nothing self._items.append(item) def size(self) -> int: # query: returns a value, no side effects return len(self._items)stack = CleanStack()stack.push("a")stack.size() # 1 — callers can reason about each call in isolation
Self-Documenting Code over Comments
Replacing an explanatory comment with named variables and functions that make the comment unnecessary.
# Before: comment explains WHAT the code does because the code itself doesn'tdef is_eligible(user): # check if user is over 18 and has verified their email and is not banned return user.age > 18 and user.email_verified and not user.banned# After: the expression explains itself; a comment would only repeat itdef is_eligible(user): is_adult = user.age > 18 has_verified_email = user.email_verified is_in_good_standing = not user.banned return is_adult and has_verified_email and is_in_good_standing# Good comments explain WHY, not WHAT:# Retry once — the payment gateway occasionally drops the first TLS handshake.def charge_with_retry(card, amount): try: return gateway.charge(card, amount) except TimeoutError: return gateway.charge(card, amount)
Function Argument Guidelines
Rules of thumb for keeping parameter lists honest and easy to call correctly.
- Prefer 0-2 arguments- Niladic and monadic functions are easiest to test; beyond 3 params, bundle related ones into an object
- Avoid boolean flag arguments- render(report, True) forces the reader to check the signature; split into renderInline(report) / renderAsPdf(report) instead
- Avoid output arguments- appendTo(buffer, text) that mutates buffer is less obvious than buffer = append(buffer, text) which returns a new value
- Keep argument order consistent- If several functions take (source, destination), don't flip the order in just one of them
- Use keyword-only args for clarity- def resize(image, *, width, height) prevents resize(img, 100, 200) ambiguity about which number is which
- Don't pass null/None to signal 'skip this'- Prefer overloaded constructors, default values, or a builder over a function that special-cases None
A function should do one thing, do it well, and do it only — if you need the word 'and' to describe what a function does, that's a sign it should be split in two.