Clean Code: Naming, Functions, and Comments
Every line of code you write will be read many more times than it was written. Studies suggest developers spend 10 times more time reading code than writing it. This isn't a minor inefficiency—it's...
Key Insights
- Names should reveal intent so completely that comments become unnecessary—if you need a comment to explain a variable, rename the variable instead.
- Functions should do exactly one thing, take few arguments, and have no side effects; if you’re passing a boolean flag, you’ve already written two functions pretending to be one.
- Comments lie over time because they don’t compile—treat every comment as a failure to express yourself in code, and reserve them only for explaining why, never what.
Why Clean Code Matters
Every line of code you write will be read many more times than it was written. Studies suggest developers spend 10 times more time reading code than writing it. This isn’t a minor inefficiency—it’s the primary bottleneck in software development.
Robert C. Martin’s “Clean Code” and Martin Fowler’s work on refactoring established a fundamental truth: the only way to go fast is to go well. Technical debt compounds. That “quick hack” you wrote last sprint? It’s now slowing down three developers who need to understand it before making changes.
Clean code isn’t about aesthetics or personal preference. It’s about reducing the cognitive load required to understand, modify, and extend software. The three pillars we’ll cover—naming, functions, and comments—form the foundation of readable code.
The Art of Meaningful Names
Names are everywhere in software. Variables, functions, classes, packages, files—we name things constantly. Getting names right is the single highest-leverage activity for code clarity.
Intention-Revealing Names
Every name should answer three questions: why it exists, what it does, and how it’s used. If a name requires a comment, it’s not intention-revealing.
# Bad: What is d? Days? Distance? Duration?
d = 7
t = d * 24
# Good: The name tells the complete story
retention_period_days = 7
retention_period_hours = retention_period_days * 24
The problem with cryptic names isn’t that they’re short—it’s that they force readers to build a mental mapping. Every time someone reads d, they must remember “oh right, that means days.” This mental overhead accumulates across a codebase.
// Bad: Mental mapping required
function calc(u, p, d) {
return u.age >= 18 && p.status === 'active' && d < 30;
}
// Good: No translation needed
function isEligibleForDiscount(user, promotion, daysSinceLastPurchase) {
const isAdult = user.age >= 18;
const hasActivePromotion = promotion.status === 'active';
const isRecentCustomer = daysSinceLastPurchase < 30;
return isAdult && hasActivePromotion && isRecentCustomer;
}
Class Names and Method Names
Follow a simple convention: classes are nouns, methods are verbs. A Customer has behavior like placeOrder() or calculateLoyaltyPoints(). You wouldn’t name a class ProcessPayment or a method customer().
// Bad: Verb as class name, noun as method
class ProcessOrder {
public void order() { }
}
// Good: Noun class, verb methods
class Order {
public void process() { }
public void cancel() { }
public boolean isShippable() { }
}
For boolean methods, use prefixes like is, has, can, or should. Reading if (user.isActive()) flows naturally; if (user.active()) does not.
Functions That Do One Thing
If there’s one rule that transforms code quality, it’s this: functions should do one thing, do it well, and do it only.
Single Responsibility at the Function Level
How do you know if a function does one thing? Try to extract another function from it that isn’t merely a restatement of its implementation. If you can, it’s doing more than one thing.
# Bad: This function does three things
def process_user_registration(email, password, name):
# Validate input
if not email or '@' not in email:
raise ValueError("Invalid email")
if len(password) < 8:
raise ValueError("Password too short")
# Create user
user = User(email=email, name=name)
user.password_hash = hash_password(password)
db.save(user)
# Send welcome email
template = load_template('welcome.html')
send_email(email, "Welcome!", template.render(name=name))
return user
# Good: Each function has one job
def process_user_registration(email, password, name):
validate_registration_input(email, password)
user = create_user(email, password, name)
send_welcome_email(user)
return user
def validate_registration_input(email, password):
if not email or '@' not in email:
raise ValueError("Invalid email")
if len(password) < 8:
raise ValueError("Password too short")
def create_user(email, password, name):
user = User(email=email, name=name)
user.password_hash = hash_password(password)
db.save(user)
return user
def send_welcome_email(user):
template = load_template('welcome.html')
send_email(user.email, "Welcome!", template.render(name=user.name))
Argument Count and Flag Arguments
The ideal number of function arguments is zero. One is acceptable. Two is tolerable. Three requires serious justification. More than three? Wrap them in an object.
Flag arguments are particularly insidious. A boolean parameter means the function does two things—one when the flag is true, another when false.
// Bad: What does 'true' mean here?
renderPage(user, true);
// Worse: The implementation reveals the problem
function renderPage(user: User, isAdmin: boolean) {
if (isAdmin) {
// 20 lines of admin rendering
} else {
// 20 lines of regular rendering
}
}
// Good: Two functions with clear purposes
renderUserPage(user);
renderAdminPage(user);
The Right Level of Abstraction
Functions should contain statements at one level of abstraction. Mixing high-level business logic with low-level implementation details creates cognitive whiplash.
# Bad: Mixed abstraction levels
def generate_monthly_report(company_id):
# High level: get company
company = Company.objects.get(id=company_id)
# Low level: SQL and string manipulation
cursor = connection.cursor()
cursor.execute(f"SELECT * FROM transactions WHERE company_id = {company_id}")
rows = cursor.fetchall()
# High level: calculate totals
total_revenue = calculate_revenue(rows)
# Low level: file operations
filename = f"/tmp/report_{company_id}_{datetime.now().strftime('%Y%m%d')}.pdf"
with open(filename, 'wb') as f:
f.write(generate_pdf_bytes(company, total_revenue))
return filename
# Good: Consistent abstraction level
def generate_monthly_report(company_id):
company = fetch_company(company_id)
transactions = fetch_monthly_transactions(company)
report_data = compile_report_data(company, transactions)
return save_report_as_pdf(report_data)
The Step-Down Rule states that code should read like a top-down narrative. Each function should be followed by those at the next level of abstraction, allowing you to read the program by descending one level at a time.
Comments: When They Help and When They Lie
Here’s an uncomfortable truth: comments are, at best, a necessary evil. At worst, they’re lies waiting to happen.
Comments don’t compile. When code changes and comments don’t, they become misinformation. The only truly reliable documentation is the code itself.
Comments as Failure
Every comment represents a failure to express yourself in code. Before writing a comment, ask: can I rename something to make this clear?
// Bad: Comment explains what the code does
// Check if employee is eligible for benefits
if (employee.type == 'FT' && employee.tenure > 90) {
// ...
}
// Good: Code explains itself
if (employee.isEligibleForBenefits()) {
// ...
}
// Inside Employee class
public boolean isEligibleForBenefits() {
return isFullTime() && hasCompletedProbationPeriod();
}
When Comments Are Justified
Some comments earn their place:
- Legal comments: Copyright and license information
- Informational comments: Explaining regex patterns or complex algorithms
- Warning comments: Alerting to consequences (
// This test takes 30 minutes to run) - TODO comments: Marking future work (but clean them up regularly)
- Explaining why, not what: Business rule justifications
# Good: Explains WHY, not what
def calculate_tax(income):
# IRS Publication 15-T requires rounding to nearest dollar
# before applying tax brackets (see section 3.2)
rounded_income = round(income)
return apply_tax_brackets(rounded_income)
Comments That Lie
The most dangerous comments are those that contradict the code:
// Returns the user's full name
function getUserName(user) {
return user.email; // Someone changed this but not the comment
}
Commented-out code is another offender. Delete it. Version control exists for a reason.
Putting It All Together
Here’s a real-world refactoring that applies these principles:
# Before: A mess of concerns and unclear intent
def proc(d):
# process the data
r = []
for i in d:
if i['t'] == 'A' and i['v'] > 100: # check if valid
x = i['v'] * 0.1 # calculate discount
i['d'] = x
r.append(i)
# save to db
for i in r:
db.execute(f"INSERT INTO discounts VALUES ({i['id']}, {i['d']})")
return len(r) # return count
# After: Clean, readable, maintainable
def apply_premium_discounts(transactions):
eligible_transactions = filter_premium_eligible(transactions)
discounted_transactions = calculate_discounts(eligible_transactions)
persist_discounts(discounted_transactions)
return len(discounted_transactions)
def filter_premium_eligible(transactions):
return [t for t in transactions
if t['type'] == 'PREMIUM' and t['value'] > PREMIUM_THRESHOLD]
def calculate_discounts(transactions):
for transaction in transactions:
transaction['discount'] = transaction['value'] * PREMIUM_DISCOUNT_RATE
return transactions
def persist_discounts(transactions):
discount_records = [(t['id'], t['discount']) for t in transactions]
db.bulk_insert('discounts', discount_records)
Code Review Checklist
When reviewing code for cleanliness, ask:
- Can I understand each variable’s purpose without context?
- Does each function do exactly one thing?
- Are functions short enough to fit on one screen?
- Do comments explain why, not what?
- Is there any commented-out code to delete?
- Are abstraction levels consistent within functions?
Conclusion
Clean code isn’t a destination—it’s a practice. The Boy Scout Rule applies: leave the code cleaner than you found it. Every small improvement compounds over time.
Start with naming. When names are clear, comments become unnecessary. When functions do one thing, testing becomes trivial. When abstraction levels are consistent, code reads like prose.
Pick up Robert C. Martin’s “Clean Code” if you haven’t. Configure your linter to enforce these principles automatically. Most importantly, practice deliberately—refactor one function today, and tomorrow the codebase will thank you.