Clean Code A: Good Names, Small Functions and a Single Responsibility
- Explain what Clean Code is and why code is read far more often than it is written
- Choose clear names for variables, functions and files according to Python’s conventions
- Recognize a function that does more than one thing and break it apart
- Apply the Single Responsibility principle at the function level
What will we learn?
Until now the goal was “make the code work”. From today, the goal is also to make it clear. We will learn:
- Naming — how to choose names for variables, functions and files.
- Small Functions — why functions should be short.
- Single Responsibility — every function does one thing.
Why do we need this?
Code is read far more often than it is written. You write a function once, but it is read on every bug fix, every new feature, and every Code Review by a teammate. And in a month, you will read it like a stranger too.
In companies, code that works but is not readable is rejected in Code Review. In job interviews they check not only “did you solve it”, but also “can the solution be understood”.
Compare:
def f(l):
t = 0
for x in l:
t = t + x[1] * x[2]
return t * 1.17VAT_RATE = 1.17
def calculate_order_total(items):
subtotal = 0
for item in items:
subtotal = subtotal + item["price"] * item["quantity"]
return subtotal * VAT_RATEBoth functions do exactly the same thing. The second one is understood without effort.
Explanation from the basics
What is Clean Code?
Clean Code is code that another person can read, understand and change easily, and with confidence. It is not a set of rigid rules, but a collection of habits that make code clear.
Naming — good names
A good name answers the question “what is this?” or “what does this do?” without having to read the surrounding code.
Python’s naming conventions (according to PEP 8, the official style guide):
| What | Style | Example |
|---|---|---|
| Variables and functions | snake_case | total_price, send_email() |
| Constants (values that do not change) | UPPER_SNAKE_CASE | VAT_RATE, MAX_RETRIES |
| Classes (the OOP topic) | PascalCase | Student, BankAccount |
| Files and modules | snake_case.py | order_utils.py |
Rules for a good name:
- Describes intent —
days_until_examis better thand. - Function = verb —
calculate_total(),send_invoice(),is_valid_email(). Variable = noun —total,invoice,email. - A boolean sounds like a question —
is_active,has_permission,can_edit. - A list in the plural —
students(a list),student(one item). - No unclear abbreviations —
customer, notcstmr. (Well-known abbreviations likeid,url,maxare fine.) - No Magic Numbers — instead of
* 1.17that nobody knows the meaning of, a named constant:VAT_RATE = 1.17.
Small Functions
A long function (50+ lines) is hard to understand, hard to test and hard to change. Rule of thumb: if a function does not fit on one screen, or it is hard to name it without the word “and”, it probably does too much.
Single Responsibility
Single Responsibility Principle (SRP): every function should have one reason to change, that is, one job.
A restaurant has a waiter, a cook and a cashier. If the cook also takes orders, cooks and collects money, every change (a new menu, a new card reader) requires “changing” him, and every mistake of his hurts everything. When each one has one job, it is easy to replace, improve and find where the problem is.
Syntax
There is no “new syntax” for clean code. It is the same Python, just organized differently. The pattern:
CONSTANT_NAME = value # constants at the top, UPPER_SNAKE_CASE
def verb_noun(clear_parameter): # a function does ONE thing, named by what it does
...
return result
def main(): # one function that connects the small pieces
data = get_something()
result = process_something(data)
show_something(result)
main()First code example
Before — code that works but is not clean. week-2/grades_before.py:
def do(g):
s = 0
for x in g:
s = s + x
a = s / len(g)
if a >= 55:
print("avg", a, "pass")
else:
print("avg", a, "fail")
do([80, 45, 92, 60])After — the same behavior, clean code. week-2/grades_after.py:
PASSING_GRADE = 55
def calculate_average(grades):
total = 0
for grade in grades:
total = total + grade
return total / len(grades)
def is_passing(average):
return average >= PASSING_GRADE
def print_report(average):
if is_passing(average):
status = "pass"
else:
status = "fail"
print("Average:", average, "-", status)
student_grades = [80, 45, 92, 60]
average = calculate_average(student_grades)
print_report(average)Output (the “after” version. The old version prints avg 69.25 pass, the same calculation worded less clearly):
Average: 69.25 - passBreaking down the code line by line
| In the old code | In the clean code | Why it is better |
|---|---|---|
do | Three functions with names | The name says what each one does. |
g, s, x, a | grades, total, grade, average | No need to “decode”. |
55 inside the code | PASSING_GRADE = 55 | A value with a meaning and a name. If the grade changes, you change it in one place. |
| Calculation + decision + printing in one function | calculate_average, is_passing, print_report | Every function does one thing. |
The practical advantage: want to show the average on a website instead of on the screen? Only print_report changes. Want to change the passing grade? Only PASSING_GRADE. And calculate_average is useful in other places too, because it returns a value instead of printing it.
Another example
A function “that does everything” in a registration system, and how to break it apart. Before:
def register(n, e, a):
if "@" not in e:
print("bad email")
return
if a < 16:
print("too young")
return
u = n.strip().lower().replace(" ", ".")
print("Welcome " + n + "! Your username is " + u)After:
MIN_AGE = 16
def is_valid_email(email):
return "@" in email
def is_old_enough(age):
return age >= MIN_AGE
def create_username(full_name):
return full_name.strip().lower().replace(" ", ".")
def register_user(full_name, email, age):
if not is_valid_email(email):
print("Invalid email address")
return
if not is_old_enough(age):
print("You must be at least", MIN_AGE, "to register")
return
username = create_username(full_name)
print("Welcome " + full_name + "! Your username is " + username)
register_user("Dana Cohen", "dana@mail.com", 24)
register_user("Avi Levi", "avi-mail.com", 30)Output:
Welcome Dana Cohen! Your username is dana.cohen
Invalid email addressBreakdown:
"@" in email—inchecks whether a piece of text appears inside another text, and returnsTrue/False..strip()— removes spaces from the start and the end..lower()— lowercase letters..replace(" ", ".")— replaces spaces with dots. These are methods ofstrthat can be chained one after the other.returnwithout a value — leaves the function immediately (returnsNone). That way we do not continue to the registration after an error. The technique is called Early Return (or Guard Clause).register_usernow “reads like a story”: check the email, check the age, create a username, greet. All the details are in small functions.
Common mistake
Mistake 1: names that lie.
def get_user(user_id):
user = find_in_list(user_id)
user["last_login"] = "today" # changes data!
delete_old_sessions(user) # deletes data!
return userThe name says “get”, but the function changes and deletes. Whoever calls it to “just get a user” will be surprised. A name should describe everything the function does. And if it is hard to describe without “and”, there is more than one responsibility.
Mistake 2: over-splitting. The opposite is also a problem:
def add_one(number):
return number + 1A one-line function that hides an obvious operation only adds jumps while reading. Split when there is an idea with a name (is_old_enough), not every line.
Mistake 3: names that are too generic. data, info, temp, result, manager, process(). Names like these can almost always be improved: data → student_records, process() → apply_discount().
Practice
The exercises use only what was taught in this lesson and the lessons before it. Try on your own first, and only then open the solution.
Exercise 1: Names that explain themselves
The following code works, but it is impossible to understand what it does:
def f(a, b):
c = a * b
if c > 500:
c = c - 50
return c
print(f(120, 5))
print(f(30, 2))Rewrite it with good names for the function and the variables, and replace the numbers 500 and 50 with named constants. The output must stay the same:
550
60Show solution
DISCOUNT_THRESHOLD = 500
DISCOUNT_AMOUNT = 50
def calculate_order_price(unit_price, quantity):
price = unit_price * quantity
if price > DISCOUNT_THRESHOLD:
price = price - DISCOUNT_AMOUNT
return price
print(calculate_order_price(120, 5))
print(calculate_order_price(30, 2))550
60- The function’s name is a verb that describes what it does.
500and50were Magic Numbers — now it is clear what they are, and if the discount changes, you change it in one place.
Exercise 2: Naming conventions
For each name — is it written according to Python’s conventions? If not, write the correct name.
TotalPrice (a variable)
numberofstudents (a variable)
max_grade (a variable)
vat_rate (a constant that never changes)
GetAverage (a function)Show solution
| Name | Correct? | Correct name |
|---|---|---|
TotalPrice | No — variables are written in snake_case | total_price |
numberofstudents | No — hard to read without underscores | number_of_students |
max_grade | Yes | — |
vat_rate | No — constants are written in capital letters | VAT_RATE |
GetAverage | No — functions are written in snake_case, and a verb is better | get_average / calculate_average |
Exercise 3: One function — one responsibility
The following function calculates an average, decides whether the student passed, and also prints — three jobs:
def report(name, grades):
total = 0
for g in grades:
total = total + g
avg = total / len(grades)
if avg >= 55:
print(name + ": average", avg, "- passed")
else:
print(name + ": average", avg, "- failed")
report("Dana", [80, 85])
report("Avi", [50, 46])Break it into three functions: calculate_average, is_passing and print_report(name, average). print_report receives an average that was already calculated and only prints. The output must stay:
Dana: average 82.5 - passed
Avi: average 48.0 - failedShow solution
PASSING_GRADE = 55
def calculate_average(grades):
return sum(grades) / len(grades)
def is_passing(average):
return average >= PASSING_GRADE
def print_report(name, average):
if is_passing(average):
status = "passed"
else:
status = "failed"
print(name + ": average", average, "-", status)
dana_average = calculate_average([80, 85])
print_report("Dana", dana_average)
avi_average = calculate_average([50, 46])
print_report("Avi", avi_average)Dana: average 82.5 - passed
Avi: average 48.0 - failedcalculate_averageonly calculates,is_passingonly decides, andprint_reportreceives a ready average and only displays it. The calculation happens outside it.- If
print_reportcalledcalculate_averageitself, it would again both calculate and print. The code is shorter, but the responsibilities are not separated. - Now
calculate_averagecan also be used in a place that does not need to print.
Exercise 4: No magic numbers
In an online store, the following function receives the order total and returns the amount to pay:
def final_price(total):
if total < 200:
total = total + 30
return total
print(final_price(150))
print(final_price(250))- What do 200 and 30 mean in the store? (Hint: what happens to a small order?) Give them names as constants.
- Rewrite the function so that its name explains exactly what it adds.
Expected output:
180
250Show solution
FREE_SHIPPING_MINIMUM = 200
SHIPPING_FEE = 30
def add_shipping(order_total):
if order_total < FREE_SHIPPING_MINIMUM:
return order_total + SHIPPING_FEE
return order_total
print(add_shipping(150))
print(add_shipping(250))180
250200 is the minimum amount for free shipping, and 30 is the shipping fee. With names, nobody has to guess.
Exercise 5: A small function that answers one question
Write a function is_valid_password(password) that returns True if the password is at least 8 characters long and contains at least one digit, otherwise False. The function does not print — it only returns.
print(is_valid_password("abc12345"))
print(is_valid_password("abcdefgh"))
print(is_valid_password("a1"))Expected output:
True
False
FalseShow solution
MIN_PASSWORD_LENGTH = 8
def has_digit(text):
for char in text:
if char.isdigit():
return True
return False
def is_valid_password(password):
return len(password) >= MIN_PASSWORD_LENGTH and has_digit(password)
print(is_valid_password("abc12345"))
print(is_valid_password("abcdefgh"))
print(is_valid_password("a1"))True
False
FalseThe name is_valid_password starts with is_ — it is clear that it returns True or False. The digit check was separated into a small function of its own.
Summary
- Clean Code = code that others (and you in a month) understand and change easily. Code is read far more often than it is written.
- Names:
snake_casefor variables and functions,UPPER_SNAKE_CASEfor constants. Function = verb, boolean = question (is_...). - No magic numbers. A value with a meaning gets a named constant.
- Small functions with a single responsibility (SRP). If the name needs “and”, you probably need to split.
- A function that calculates returns. Printing is a separate responsibility.