# Clean Code B: DRY, KISS, Separation of Concerns, Comments and Refactoring<no value>

## What will we learn?

- **DRY** — Don't Repeat Yourself.
- **KISS** — Keep It Simple, Stupid.
- **Separation of Concerns** — separating responsibilities between parts of the program.
- **Comments** — when a comment helps and when it hurts.
- **Refactoring** — improving the structure of the code without changing its behavior.

## Why do we need this?

In the topic on the computer and the operating system we wrote `project_check.py` with four almost identical blocks. What happens when we need to check 20 more files? Or change the message from `OK` to `✓`? You have to touch 20 places, and in one of them you will surely forget.

Today's principles answer the question: **how do you write code that is easy to change?** In reality, most of a developer's time goes into changing existing code, not writing new code.

## Explanation from the basics

### DRY — Don't Repeat Yourself

**Every piece of knowledge should appear in one place only.** When the same logic appears in several places:

- A bug fix has to be made in all the places, and one is always forgotten.
- The code is longer, and it is hard to see what is unique in each part.

**The tools for fixing it:** a function (logic that repeats), a loop (an action on many items), a constant (a value that repeats).

**Warning sign:** if you Copy-Paste code and change a small detail in it, you probably need a function with a parameter.

### KISS — Keep It Simple

**The simplest solution that works is usually the best one.** "Clever" code (one complicated line, tricks) is hard to read, hard to debug and hard to change. The goal of code is to be understood, not to impress.

### Separation of Concerns

**Concern** = an area of responsibility. Every program has a few different areas:

- **Input** — where the information comes from (keyboard, file, HTTP request).
- **Logic** — what we do with it (calculations, decisions, rules).
- **Output** — where the information goes (screen, file, HTTP response).

When they are separated, you can replace one without touching the others. **This is exactly the structure of a server in the backend and databases topics:** the Route (HTTP input/output) is separated from the database access code (Database).

SRP (from the previous lesson) talks about a single function. Separation of Concerns talks about **the whole program**: which parts exist, and how they are separated.

### Comments

**The code explains "what" and "how". A comment explains "why".**

| A good comment | An unnecessary / harmful comment |
|---|---|
| Explains **why** we chose a particular way | Repeats what the code already says |
| Warns about unexpected behavior | Makes up for a bad name |
| Points to a source (a law, a standard, a document) | "Dead code" that was turned into a comment |
| | An old comment that is no longer true, which is the worst |

### Refactoring

**Refactoring** = changing the **structure** of the code **without changing its behavior**. You rename, split functions, remove duplicated code. At the end, the code does exactly the same thing, only more clearly.

**The important rule:** **small steps, and a check after every step.** Change one thing, run, compare with the original output, and continue.

## Syntax

```python
# DRY: repeated code  →  one function + loop
def check(item):
    ...

for item in items:
    check(item)

# Comment: explain WHY, not WHAT
# Tax authority requires rounding up to whole shekels (regulation 12b)
total = math.ceil(total)

# Docstring: a string right under def that describes the function
def calculate_shipping(weight_kg):
    """Return shipping cost in NIS for a package of the given weight."""
    ...
```

A **Docstring** is a string in triple quotes (`"""..."""`) that appears on the first line of a function and describes what it does. VS Code shows it when you hover over a call to the function.

## First code example

**DRY in action.** Remember `project_check.py` from the topic on the computer, the operating system and the network? **Before:**

```python
import os

base = "../shop"

if os.path.exists(base + "/data"):
    print("data -> OK")
else:
    print("data -> MISSING")

if os.path.exists(base + "/logs"):
    print("logs -> OK")
else:
    print("logs -> MISSING")

if os.path.exists(base + "/app.py"):
    print("app.py -> OK")
else:
    print("app.py -> MISSING")
```

**After:**

```python
import os

PROJECT_DIR = "../shop"
REQUIRED_PATHS = ["data", "logs", "app.py", "data/products.txt"]

def check_path(relative_path):
    full_path = PROJECT_DIR + "/" + relative_path
    if os.path.exists(full_path):
        status = "OK"
    else:
        status = "MISSING"
    print(relative_path, "->", status)

for path in REQUIRED_PATHS:
    check_path(path)
```

**Output (the same behavior):**

```text
data -> OK
logs -> OK
app.py -> OK
data/products.txt -> OK
```

## Breaking down the code line by line

| Line | Explanation |
|---|---|
| `PROJECT_DIR = "../shop"` | The path is defined **once**. If the project moves to another folder, you change one line. |
| `REQUIRED_PATHS = [...]` | The **data** (what to check) is separated from the **logic** (how to check). Adding a check = adding a name to the list. |
| `def check_path(relative_path):` | The logic that repeated four times, written **once**. The part that changed between the blocks (the path name) became a **parameter**. |
| `status = ...` and then one `print` | Instead of two almost identical `print`s, a variable with the status and one print. That is also DRY on a small scale. |
| `for path in REQUIRED_PATHS:` | The loop runs the check on every item. |

## Another example

**KISS + Separation of Concerns + comments.** A program that calculates a shipping cost. **Before** — "clever", mixed together and with unnecessary comments:

```python
w = float(input("w: "))  # get w
# calculate the price
print("Price:", (w <= 1) * 20 + (1 < w <= 5) * 35 + (w > 5) * (35 + (w - 5) * 4))  # print
```

**After:**

```python
BASE_PRICE_SMALL = 20
BASE_PRICE_MEDIUM = 35
PRICE_PER_EXTRA_KG = 4
SMALL_MAX_KG = 1
MEDIUM_MAX_KG = 5

def read_weight():
    return float(input("Package weight (kg): "))

def calculate_shipping(weight_kg):
    """Return the shipping price in NIS for a package of the given weight."""
    if weight_kg <= SMALL_MAX_KG:
        return BASE_PRICE_SMALL
    if weight_kg <= MEDIUM_MAX_KG:
        return BASE_PRICE_MEDIUM
    # Above the medium tier the courier charges per extra kg (price list 2026)
    extra_kg = weight_kg - MEDIUM_MAX_KG
    return BASE_PRICE_MEDIUM + extra_kg * PRICE_PER_EXTRA_KG

def show_price(price):
    print("Shipping price:", price, "NIS")

weight = read_weight()
price = calculate_shipping(weight)
show_price(price)
```

**Run:**

```text
Package weight (kg): 7.5
Shipping price: 45.0 NIS
```

**Breakdown:**

- **KISS:** the trick `(w <= 1) * 20` works (because `True` counts as 1 and `False` as 0), but nobody will understand it at a glance. A simple chain of `if`s is readable immediately.
- **Separation of Concerns:** `read_weight` (input), `calculate_shipping` (logic), `show_price` (output). Tomorrow the weight comes from an HTTP request? You replace only `read_weight`. The logic does not change.
- **Comments:** `# get w` and `# print` were deleted, because they repeated the code. One comment remained that explains **why** (the courier company's price list), information that cannot be understood from the code itself.
- The **Docstring** describes what the function returns and in which units (NIS).

## Common mistake

**Mistake 1: a comment instead of a good name.**

```python
d = 30  # number of days until the subscription expires
```

Better: `days_until_expiry = 30`, with no comment at all. If you need a comment to explain a variable, the name is not good enough.

**Mistake 2: a comment that lies.**

```python
# Discount is 10%
price = price * 0.85
```

Someone updated the code and forgot the comment. Now the reader does not know what to believe. Comments are not checked by Python, so they "rot". That is another reason to write few comments, and good ones.

**Mistake 3: DRY too early.** Two pieces of code that look similar **by coincidence**, but represent different business rules, do not have to be merged. For example, "17% VAT" and "17% commission" are a coincidence, not the same knowledge. If they are merged into one constant, changing one will break the other.

**Mistake 4: a "big" Refactoring all at once.** You change 10 things, run, something breaks, and you have no idea what. **One step, run, compare, next step.**

<!-- 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: DRY — a receipt without copy-paste

```python
print("Pen", 5, "x", 4, "=", 5 * 4)
print("Notebook", 12, "x", 2, "=", 12 * 2)
print("Bag", 60, "x", 1, "=", 60 * 1)
print("Total:", 5 * 4 + 12 * 2 + 60 * 1)
```

Rewrite the code so that the data is kept in lists, and one function with a loop prints the receipt. The output must stay:

```text
Pen 5 x 4 = 20
Notebook 12 x 2 = 24
Bag 60 x 1 = 60
Total: 104
```

{{< solution >}}
```python
def line_total(price, quantity):
    return price * quantity


def print_receipt(names, prices, quantities):
    total = 0
    for i in range(len(names)):
        amount = line_total(prices[i], quantities[i])
        print(names[i], prices[i], "x", quantities[i], "=", amount)
        total = total + amount
    print("Total:", total)


print_receipt(["Pen", "Notebook", "Bag"], [5, 12, 60], [4, 2, 1])
```

```text
Pen 5 x 4 = 20
Notebook 12 x 2 = 24
Bag 60 x 1 = 60
Total: 104
```

Every calculation now appears **once**. A fourth product = another value in the lists, without another line of code.
{{< /solution >}}

### Exercise 2: KISS — code that is too "clever"

```python
def parity(n):
    return (n % 2 == 0) * "even" + (n % 2 == 1) * "odd"

print(parity(4))
print(parity(7))
```

The code works, but it is hard to understand. Rewrite `parity` in the simplest, most readable way.

**Expected output:**

```text
even
odd
```

{{< solution >}}
```python
def parity(n):
    if n % 2 == 0:
        return "even"
    return "odd"


print(parity(4))
print(parity(7))
```

```text
even
odd
```

**KISS** — Keep It Simple. A regular `if` reads immediately, without thinking about how multiplying `True` by a string works.
{{< /solution >}}

### Exercise 3: Comments — why, not what

For each comment — is it good, unnecessary or harmful? Explain.

```text
1.  count = count + 1   # add 1 to count
2.  # 30 days: the refund policy of the shop
    REFUND_DAYS = 30
3.  total = price * 2   # multiply price by 3
4.  # retry 3 times — the payment API sometimes fails on the first call
```

{{< solution >}}
1. **Unnecessary** — it repeats what the code already says.
2. **Good** — it explains **why** 30, information that does not appear in the code.
3. **Harmful** — it lies (the code multiplies by 2). A comment that was not updated is worse than no comment.
4. **Good** — it explains **why** the code does something that looks strange.

The rule: the code says **what**, the comment says **why**.
{{< /solution >}}

### Exercise 4: Separation of concerns — calculation versus input and output

```python
def bmi():
    weight = float(input("Weight: "))
    height = float(input("Height: "))
    print("BMI:", round(weight / (height * height), 1))
```

The function does three things: it reads input, calculates and prints. So the calculation cannot be used without a keyboard. Break it into three functions, one per responsibility:

1. `read_number(message)` — only reads a number from the user and returns it.
2. `calculate_bmi(weight, height)` — only calculates and returns, with no `input` and no `print`.
3. `print_bmi(bmi)` — only prints a result that was already calculated.

Test the calculation and the printing without a keyboard:

```py
bmi = calculate_bmi(70, 1.75)
print_bmi(bmi)
```

**Expected output:**

```text
BMI: 22.9
```

{{< solution >}}
```python
def read_number(message):
    return float(input(message))


def calculate_bmi(weight, height):
    return round(weight / (height * height), 1)


def print_bmi(bmi):
    print("BMI:", bmi)


bmi = calculate_bmi(70, 1.75)
print_bmi(bmi)
```

```text
BMI: 22.9
```

In the real program the three functions are connected, and each line runs one responsibility:

```py
weight = read_number("Weight: ")
height = read_number("Height: ")
bmi = calculate_bmi(weight, height)
print_bmi(bmi)
```

- `input` is only in `read_number`, and `print` is only in `print_bmi`. `calculate_bmi` has no input and no output, so it can be called with data from anywhere: from input, from a file, or in a test.
- `print("BMI:", calculate_bmi(70, 1.75))` is shorter code, but the printing is still glued to the calculation on the same line, and no function is responsible for the display. Want to show the result differently? With the separation you only replace `print_bmi`.
{{< /solution >}}

### Exercise 5: Refactoring in steps

```python
t = 12
if t < 15:
    print(12, "cold")
elif t < 28:
    print(12, "pleasant")
else:
    print(12, "hot")
t = 24
if t < 15:
    print(24, "cold")
elif t < 28:
    print(24, "pleasant")
else:
    print(24, "hot")
```

Refactor it: a function `classify(temperature)` that returns the classification, constants for the limits, and a loop over the list of temperatures `[12, 24, 33]`.

**Expected output:**

```text
12 cold
24 pleasant
33 hot
```

{{< solution >}}
```python
COLD_LIMIT = 15
HOT_LIMIT = 28


def classify(temperature):
    if temperature < COLD_LIMIT:
        return "cold"
    if temperature < HOT_LIMIT:
        return "pleasant"
    return "hot"


for temperature in [12, 24, 33]:
    print(temperature, classify(temperature))
```

```text
12 cold
24 pleasant
33 hot
```

The steps: (1) extract a function, (2) move 15 and 28 into constants, (3) replace the duplicated code with a loop — and run after **every** step to make sure the output did not change.
{{< /solution >}}

<!-- exercises:end -->

## Summary

- **DRY** — every piece of knowledge in one place. Repeating code → a function, a loop or a constant.
- **KISS** — simple and readable is better than "clever".
- **Separation of Concerns** — input, logic and output in separate parts. That is the structure of every server.
- **Comments** explain **why**. The code itself (with good names) explains **what**. A comment that lies is worse than no comment.
- **Refactoring** — improve structure without changing behavior, in small steps with a run after every step.

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

## Official documentation

- [PEP 20 — The Zen of Python](https://peps.python.org/pep-0020/)
- [PEP 8 — Comments](https://peps.python.org/pep-0008/#comments)
- [Python Tutorial — Defining Functions](https://docs.python.org/3/tutorial/controlflow.html#defining-functions)

<!-- docs:end -->
