4.13 Clean Code Principles
Table of Contents
Why Clean Code Matters
Clean code is code that is easy to read, easy to understand, and easy to change. You are not only writing for the computer, you are writing for future humans, including your future self.
Imagine opening your own code 6 months later and having no idea what it does. Clean code prevents that.
Goal of clean code:
Write code that is easy to understand and safe to change.
In this chapter you will see simple but powerful habits you can use from your very first programs, no matter which language you use.
We will use Python-style pseudocode for examples, but the ideas apply to any language.
Meaningful Names
Names are the first thing people see in your code. Good names make code almost read like plain English.
Use descriptive variable and function names
Bad:
def c(a, b):
return a * b * 3.14
What is c? What are a and b?
Better:
def circle_area(radius):
return radius * radius * 3.14Now the name explains what the function does and what the parameter is.
More examples:
| Bad name | Better name | Why it is better |
|---|---|---|
x | user_age | Tells you what the value represents |
n | max_retries | Shows meaning and intent |
p | product_price | Clear business meaning |
f | read_file_content | Verb + object explains the action |
d | user_by_id | Suggests it is a mapping of id to user |
Rule: Prefer clear, longer names over short and confusing names.
If a name saves you one second to type, but costs 10 seconds every time you read it, it is a bad trade.
Use consistent naming style
Within a project, use the same style everywhere.
Common styles:
| Style | Example | Often used for |
|---|---|---|
snake_case | user_profile | Python variables, functions |
camelCase | userProfile | JavaScript variables, functions |
PascalCase | UserProfile | Classes, types |
SCREAMING_SNAKE_CASE | MAX_SIZE | Constants |
Pick the style that your language community uses and stick to it.
Bad:
userName = "Alice"
user_age = 30
UserCountry = "FR"Better:
user_name = "Alice"
user_age = 30
user_country = "FR"Avoid meaningless prefixes and comments in names
Bad:
data1 = "Alice"
data2 = 30
data3 = "FR"Better:
user_name = "Alice"
user_age = 30
user_country = "FR"Do not encode too much in the name with weird prefixes:
Bad:
strUserNm = "Alice"
intUserId = 123Your type system and IDE can already tell you types.
Make Booleans read like questions
Boolean variables and functions should sound like yes/no questions.
Bad:
flag = True
if flag:
...Better:
is_active = True
if is_active:
...Bad:
def password(user):
...Better:
def is_valid_password(user):
...Small, Focused Functions
A function should do one thing, and do it well. If you cannot describe what a function does in a short sentence, it probably does too much.
One responsibility per function
Bad:
def process_order(order):
# 1. validate order
if not order.items:
raise ValueError("No items")
# 2. calculate total
total = 0
for item in order.items:
total += item.price * item.quantity
# 3. save to database
db.save(order, total)
# 4. send email
send_email(order.user.email, "Order received", "Thanks!")This function validates, calculates, saves, and sends email. Too many responsibilities.
Better:
def validate_order(order):
if not order.items:
raise ValueError("No items")
def calculate_order_total(order):
total = 0
for item in order.items:
total += item.price * item.quantity
return total
def save_order(order, total):
db.save(order, total)
def notify_order_received(order):
send_email(order.user.email, "Order received", "Thanks!")
def process_order(order):
validate_order(order)
total = calculate_order_total(order)
save_order(order, total)
notify_order_received(order)
Now each function has a clear purpose, and process_order reads like a story.
Rule: If you can split a function into smaller functions with clear names, you probably should.
Keep functions short
There is no strict line, but many developers try to keep functions short enough to see fully on one screen, for example 10 to 30 lines.
Compare:
Bad:
def handle_user_request(request):
# 100 lines doing many things
...Better:
def handle_user_request(request):
user = authenticate(request)
validate_request(request)
data = parse_data(request)
result = execute_action(user, data)
return build_response(result)Each called function will be shorter and focused.
Avoid too many parameters
If a function has many arguments, it is harder to call correctly and to understand.
Bad:
def create_user(name, age, country, email, is_admin, is_active, created_at, updated_at):
...Better, group related data into an object or dictionary:
def create_user(user_data):
...Or at least use keyword arguments when calling:
create_user(
name="Alice",
age=30,
country="FR",
email="a@example.com",
is_admin=False,
is_active=True,
created_at=now(),
updated_at=now(),
)Comments That Add Value
Comments should explain why something is done, not repeat what is obvious from the code.
Avoid explaining the obvious
Bad:
# increase i by 1
i = i + 1The code already says that.
Better:
# move to the next page
current_page = current_page + 1Now the comment adds context.
Use comments to explain intent, not to fix bad code
Bad:
# this function validates user input and saves it to the database and sends an email
def handle_user():
...The real problem is the function does too many things. Fix the code instead:
def validate_user_input(...):
...
def save_user(...):
...
def send_welcome_email(...):
...
def handle_user():
...Now you do not need that long comment.
Document reasons, not just behavior
Good uses of comments:
- Explain why a non-obvious choice was made
- Explain why something that looks wrong is actually correct
- Warn about performance or security implications
Example:
# We use a fixed seed here so that tests are repeatable
random.seed(42)# This query intentionally does not use an index because we need the latest dataKeep comments up to date
Old comments that are no longer true are dangerous.
Bad:
# This function returns user age
def get_user_data():
return {"name": "Alice", "age": 30, "country": "FR"}If behavior changes and comment is not updated, readers are misled.
Rule: If you change code, update or remove related comments.
Consistent Formatting and Style
Formatting is not about the computer. It is for human eyes. Consistent style makes code easier to scan and read.
Indentation and spacing
Use consistent indentation, usually 2 or 4 spaces. Do not mix tabs and spaces.
Bad:
if is_valid:
print("OK")
print("Still valid")Better:
if is_valid:
print("OK")
print("Still valid")Use spaces around operators:
Bad:
total=price*quantity+taxBetter:
total = price * quantity + taxBreak long lines
Very long lines are hard to read and to see in many editors.
Bad:
send_email(user.email, "Welcome to our very cool application", "Hello " + user.name + ", thanks for registering. Please click this very long link to verify your account: " + verification_link)Better:
subject = "Welcome to our very cool application"
body = (
"Hello " + user.name +
", thanks for registering. Please click this link to verify your account: " +
verification_link
)
send_email(user.email, subject, body)Use blank lines to group logic
Blank lines help structure code visually.
Bad:
def process():
user = get_user()
if not user:
return None
data = load_data()
result = compute(user, data)
save_result(result)
return resultBetter:
def process():
user = get_user()
if not user:
return None
data = load_data()
result = compute(user, data)
save_result(result)
return resultAvoiding Repetition (DRY)
DRY stands for "Don't Repeat Yourself". Repeated code is harder to maintain, because you must change it in many places.
Extract repeated code into functions
Bad:
def send_welcome_email(user):
body = "Hello " + user.name + ", welcome!"
send_email(user.email, "Welcome", body)
def send_password_reset_email(user, token):
body = "Hello " + user.name + ", reset your password: " + token
send_email(user.email, "Reset password", body)Both build email bodies in similar ways.
Better:
def build_greeting(user):
return "Hello " + user.name + ", "
def send_welcome_email(user):
body = build_greeting(user) + "welcome!"
send_email(user.email, "Welcome", body)
def send_password_reset_email(user, token):
body = build_greeting(user) + "reset your password: " + token
send_email(user.email, "Reset password", body)Now if you want to change the greeting, you change one place.
Avoid copy-paste logic
Suppose you validate email in two places:
Bad:
def register_user(email):
if "@" not in email or "." not in email:
raise ValueError("Invalid email")
...
def update_email(user, new_email):
if "@" not in new_email or "." not in new_email:
raise ValueError("Invalid email")
...Better:
def validate_email(email):
if "@" not in email or "." not in email:
raise ValueError("Invalid email")
def register_user(email):
validate_email(email)
...
def update_email(user, new_email):
validate_email(new_email)
...Simplicity Over Cleverness
Simple code is usually better than "smart" code that is hard to understand.
Prefer clear loops over complex one-liners
Many languages let you write very short but dense statements. Use them carefully.
Compare:
# Complex one-liner
result = [x * 2 for x in numbers if x % 2 == 0]This is fine if you know the syntax, but for a beginner it might be confusing.
You can write:
result = []
for number in numbers:
if number % 2 == 0:
result.append(number * 2)Both are correct. Use the simpler form if your team is new, or if the logic is more complex.
Avoid unnecessary clever tricks
Bad:
# Using a hacky trick to convert boolean to int
value = True + True + False # 2Better:
value = int(True) + int(True) + int(False)Or simply count with clear logic.
Do not prematurely optimize
Do not try to make code "extremely fast" before you know where the real performance problem is.
Bad:
# Very complex code to avoid a tiny extra loopBetter:
- First write simple, clear code.
- Measure performance.
- Optimize only the parts that are proven slow.
Rule: Make it work, then make it clear, then, if needed, make it fast.
Defensive Programming and Error Handling Style
Clean code is also about how you handle incorrect data and unexpected situations.
(You will see full error handling techniques in a dedicated chapter, here we focus on style.)
Fail fast and clearly
When inputs are invalid, handle it early.
Bad:
def divide(a, b):
return a / b # May crash later if b is 0Better:
def divide(a, b):
if b == 0:
raise ValueError("b must not be zero")
return a / bNow the error is clear and close to the cause.
Avoid deeply nested code
Deep nesting makes code hard to read.
Bad:
def process(user):
if user is not None:
if user.is_active:
if has_permission(user):
do_something()Better, use early returns:
def process(user):
if user is None:
return
if not user.is_active:
return
if not has_permission(user):
return
do_something()Writing Simple Tests Early
Testing has its own chapter, but as a clean code habit you should think about tests as you write code.
Functions that are easy to test are usually clean
Pure functions, that take inputs and return outputs without hidden effects, are easier to test and easier to reason about.
Example of a pure function:
def calculate_total(price, quantity, tax_rate):
return price * quantity * (1 + tax_rate)This is easy to test:
assert calculate_total(10, 2, 0.1) == 22If a function reads global variables, writes files, prints to console, and returns a value, it becomes much harder to test and understand.
So by designing functions that are easy to test, you also improve cleanliness.
Clean Code in Small Programs vs Large Systems
As a beginner you might wonder if clean code is only for large applications. It is not.
Small scripts
Even a 20-line script benefits from:
- Descriptive names
- A few small functions
- Simple comments
Example:
Bad:
import sys
f = open(sys.argv[1])
c = f.read().split("\n")
for l in c:
if l != "":
print(l)Better:
import sys
def read_lines(file_path):
with open(file_path) as f:
return f.read().splitlines()
def print_non_empty_lines(lines):
for line in lines:
if line:
print(line)
def main():
file_path = sys.argv[1]
lines = read_lines(file_path)
print_non_empty_lines(lines)
if __name__ == "__main__":
main()Even though it is longer, it is much clearer.
Large backend applications
When your project grows, these small habits become essential:
- A bad name might appear in dozens of files.
- A badly structured function might be impossible to modify safely.
- Duplicated logic might cause bugs when you fix one copy but forget another.
Clean code scales better.
Practicing Clean Code
Clean code is not something you learn once. It is a habit you build.
Simple practice ideas
- After writing a function, read it out loud as if explaining to a friend. Does it sound simple?
- Rename any variable that makes you pause to remember what it is.
- Before adding a comment, ask "Can I make the code itself clearer instead?"
- Look for copy-pasted code and try to extract it into a function.
- When you review your own code, focus on making it easier to understand, not just on "Does it work?"
A small refactoring exercise
Start with this messy code:
def handle(u, a):
if a > 18:
print("ok")
print("welcome " + u)
else:
print("no")Try to turn it into cleaner code:
Possible version:
ADULT_AGE = 18
def is_adult(age):
return age >= ADULT_AGE
def greet_user(name):
print("welcome " + name)
def handle_user(name, age):
if not is_adult(age):
print("Access denied")
return
print("Access granted")
greet_user(name)Now the code explains itself.
Summary
Key ideas from this chapter:
- Use meaningful, consistent names that describe purpose.
- Keep functions small, focused, and with a single responsibility.
- Write comments that explain why, not what is obvious.
- Format code consistently with clear indentation and spacing.
- Avoid repetition by extracting common logic into functions.
- Prefer simple, clear code over clever tricks.
- Handle errors early and reduce deep nesting.
- Design functions that are easy to test.
Clean code principle: Code is read far more often than it is written.
Always optimize for the reader.
These habits will make all later backend topics, such as web frameworks, databases, and APIs, easier to understand and safer to implement.
Views: 7
KAHIBARO