Clean Code A: Good Names, Small Functions and a Single Responsibility

Lesson goals
  • 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.17
VAT_RATE = 1.17

def calculate_order_total(items):
    subtotal = 0
    for item in items:
        subtotal = subtotal + item["price"] * item["quantity"]
    return subtotal * VAT_RATE

Both 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):

WhatStyleExample
Variables and functionssnake_casetotal_price, send_email()
Constants (values that do not change)UPPER_SNAKE_CASEVAT_RATE, MAX_RETRIES
Classes (the OOP topic)PascalCaseStudent, BankAccount
Files and modulessnake_case.pyorder_utils.py

Rules for a good name:

  1. Describes intent — days_until_exam is better than d.
  2. Function = verb — calculate_total(), send_invoice(), is_valid_email(). Variable = noun — total, invoice, email.
  3. A boolean sounds like a question — is_active, has_permission, can_edit.
  4. A list in the plural — students (a list), student (one item).
  5. No unclear abbreviations — customer, not cstmr. (Well-known abbreviations like id, url, max are fine.)
  6. No Magic Numbers — instead of * 1.17 that 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.

Analogy: a restaurant

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 - pass

Breaking down the code line by line

In the old codeIn the clean codeWhy it is better
doThree functions with namesThe name says what each one does.
g, s, x, agrades, total, grade, averageNo need to “decode”.
55 inside the codePASSING_GRADE = 55A value with a meaning and a name. If the grade changes, you change it in one place.
Calculation + decision + printing in one functioncalculate_average, is_passing, print_reportEvery 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 address

Breakdown:

  • "@" in email — in checks whether a piece of text appears inside another text, and returns True/False.
  • .strip() — removes spaces from the start and the end. .lower() — lowercase letters. .replace(" ", ".") — replaces spaces with dots. These are methods of str that can be chained one after the other.
  • return without a value — leaves the function immediately (returns None). That way we do not continue to the registration after an error. The technique is called Early Return (or Guard Clause).
  • register_user now “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 user

The 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 + 1

A 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
60
Show 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.
  • 500 and 50 were 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
NameCorrect?Correct name
TotalPriceNo — variables are written in snake_casetotal_price
numberofstudentsNo — hard to read without underscoresnumber_of_students
max_gradeYes—
vat_rateNo — constants are written in capital lettersVAT_RATE
GetAverageNo — functions are written in snake_case, and a verb is betterget_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 - failed
Show 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 - failed
  • calculate_average only calculates, is_passing only decides, and print_report receives a ready average and only displays it. The calculation happens outside it.
  • If print_report called calculate_average itself, it would again both calculate and print. The code is shorter, but the responsibilities are not separated.
  • Now calculate_average can 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))
  1. What do 200 and 30 mean in the store? (Hint: what happens to a small order?) Give them names as constants.
  2. Rewrite the function so that its name explains exactly what it adds.

Expected output:

180
250
Show 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
250

200 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
False
Show 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
False

The 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_case for variables and functions, UPPER_SNAKE_CASE for 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.

Official documentation