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.
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
| Concept | Pick One | Avoid Mixing |
|---|---|---|
| Fetch data | get, fetch | Don’t use both |
| Create object | create, make, build | Pick one |
| Insert data | insert, add, append | Pick 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 Args | Readability |
|---|---|
| 0-1 | Ideal |
| 2 | Reasonable |
| 3 | Suspicious |
| >3 | Requires 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:
- Methods on itself
- Methods on parameters
- Methods on objects it creates
- 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.