# Clean Code A: Good Names, Small Functions and a Single Responsibility<no value>

## 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:

```python
def f(l):
    t = 0
    for x in l:
        t = t + x[1] * x[2]
    return t * 1.17
```

```python
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):**

| 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:**

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.

{{< box "analogy" "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.
{{< /box >}}

## Syntax

There is no "new syntax" for clean code. It is **the same** Python, just organized differently. The pattern:

```python
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`:

```python
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`:

```python
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):**

```text
Average: 69.25 - pass
```

## Breaking 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:**

```python
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:**

```python
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:**

```text
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.**

```python
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:

```python
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()`.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## 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:

```python
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:

```text
550
60
```

{{< solution >}}
```python
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))
```

```text
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.
{{< /solution >}}

### Exercise 2: Naming conventions

For each name — is it written according to Python's conventions? If not, write the correct name.

```text
TotalPrice      (a variable)
numberofstudents (a variable)
max_grade       (a variable)
vat_rate        (a constant that never changes)
GetAverage      (a function)
```

{{< 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` |
{{< /solution >}}

### Exercise 3: One function — one responsibility

The following function calculates an average, decides whether the student passed, and also prints — three jobs:

```python
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:

```text
Dana: average 82.5 - passed
Avi: average 48.0 - failed
```

{{< solution >}}
```python
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)
```

```text
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.
{{< /solution >}}

### Exercise 4: No magic numbers

In an online store, the following function receives the order total and returns the amount to pay:

```python
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:**

```text
180
250
```

{{< solution >}}
```python
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))
```

```text
180
250
```

200 is the **minimum amount for free shipping**, and 30 is the **shipping fee**. With names, nobody has to guess.
{{< /solution >}}

### 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.

```python
print(is_valid_password("abc12345"))
print(is_valid_password("abcdefgh"))
print(is_valid_password("a1"))
```

**Expected output:**

```text
True
False
False
```

{{< solution >}}
```python
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"))
```

```text
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.
{{< /solution >}}

<!-- exercises:end -->

## 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.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## Official documentation

- [PEP 8 — Naming Conventions](https://peps.python.org/pep-0008/#naming-conventions)
- [PEP 8 — Function and Variable Names](https://peps.python.org/pep-0008/#function-and-variable-names)
- [PEP 20 — The Zen of Python](https://peps.python.org/pep-0020/)

<!-- docs:end -->
