Clean Code B: DRY, KISS, Separation of Concerns, Comments and Refactoring
- Recognize duplicated code and replace it with a function or a loop (DRY)
- Prefer a simple, readable solution over a “clever” one (KISS)
- Separate input, logic and output (Separation of Concerns)
- Know when to write a comment and when the code should explain itself
- Refactor in small steps while keeping the same behavior
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
# 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:
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:
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):
data -> OK
logs -> OK
app.py -> OK
data/products.txt -> OKBreaking 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 prints, 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:
w = float(input("w: ")) # get w
# calculate the price
print("Price:", (w <= 1) * 20 + (1 < w <= 5) * 35 + (w > 5) * (35 + (w - 5) * 4)) # printAfter:
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:
Package weight (kg): 7.5
Shipping price: 45.0 NISBreakdown:
- KISS: the trick
(w <= 1) * 20works (becauseTruecounts as 1 andFalseas 0), but nobody will understand it at a glance. A simple chain ofifs 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 onlyread_weight. The logic does not change. - Comments:
# get wand# printwere 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.
d = 30 # number of days until the subscription expiresBetter: 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.
# Discount is 10%
price = price * 0.85Someone 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.
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
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:
Pen 5 x 4 = 20
Notebook 12 x 2 = 24
Bag 60 x 1 = 60
Total: 104Show solution
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])Pen 5 x 4 = 20
Notebook 12 x 2 = 24
Bag 60 x 1 = 60
Total: 104Every calculation now appears once. A fourth product = another value in the lists, without another line of code.
Exercise 2: KISS — code that is too “clever”
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:
even
oddShow solution
def parity(n):
if n % 2 == 0:
return "even"
return "odd"
print(parity(4))
print(parity(7))even
oddKISS — Keep It Simple. A regular if reads immediately, without thinking about how multiplying True by a string works.
Exercise 3: Comments — why, not what
For each comment — is it good, unnecessary or harmful? Explain.
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 callShow solution
- Unnecessary — it repeats what the code already says.
- Good — it explains why 30, information that does not appear in the code.
- Harmful — it lies (the code multiplies by 2). A comment that was not updated is worse than no comment.
- Good — it explains why the code does something that looks strange.
The rule: the code says what, the comment says why.
Exercise 4: Separation of concerns — calculation versus input and output
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:
read_number(message)— only reads a number from the user and returns it.calculate_bmi(weight, height)— only calculates and returns, with noinputand noprint.print_bmi(bmi)— only prints a result that was already calculated.
Test the calculation and the printing without a keyboard:
bmi = calculate_bmi(70, 1.75)
print_bmi(bmi)Expected output:
BMI: 22.9Show solution
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)BMI: 22.9In the real program the three functions are connected, and each line runs one responsibility:
weight = read_number("Weight: ")
height = read_number("Height: ")
bmi = calculate_bmi(weight, height)
print_bmi(bmi)inputis only inread_number, andprintis only inprint_bmi.calculate_bmihas 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 replaceprint_bmi.
Exercise 5: Refactoring in steps
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:
12 cold
24 pleasant
33 hotShow solution
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))12 cold
24 pleasant
33 hotThe 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.
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.