I opened this session by pulling up a student's code from Week 3.
It was good work - loops, conditionals, everything working correctly. But one thing stood out. The grade calculation logic appeared three times. In three different places. Word for word identical.
I put it on the screen without saying anything. Let the class look at it.
"What's wrong with this code?" I asked.
Silence. Then Brian said: "It's... the same thing written three times?"
Exactly. And that's the problem functions solve.
The DRY Principle: Don't Repeat Yourself
Before any code, I wrote three letters on the board: DRY.
Don't Repeat Yourself.
It's one of the oldest rules in programming. If you're writing the same logic in more than one place, something is wrong. What happens when the logic needs to change? You have to find every copy and update them all. Miss one, and your program behaves differently in different places. That's how bugs are born.
Functions are the solution. You write the logic once, give it a name, and call it by that name whenever you need it.
Your First Function
The syntax is straightforward:
def greet():
print("Habari! Welcome to the system.")
greet()
greet()
greet()
Habari! Welcome to the system.
Habari! Welcome to the system.
Habari! Welcome to the system.
def tells Python: I'm about to define a function.
The name (greet) is what you'll use to call it later.
The colon and indented block is the function body — the code that runs every time you call it.
Simple. But not very useful yet - it does the same thing every time. That's where parameters come in.
Parameters: The Ingredients You Swap Out
My analogy for functions: a recipe card. You write the recipe once - the steps, the method, the timing. But the ingredients can change. Chapati with butter. Chapati with jam. Same recipe, different inputs.
Parameters are the ingredients:
def greet(name):
print(f"Habari {name}! Welcome to the system.")
greet("Amina")
greet("Otieno")
greet("Wanjiku")
Habari Amina! Welcome to the system.
Habari Otieno! Welcome to the system.
Habari Wanjiku! Welcome to the system.
One function. Three different results. The name changes - the recipe doesn't.
Multiple parameters work exactly as you'd expect:
def introduce(name, course, city):
print(f"Name: {name} | Course: {course} | From: {city}")
introduce("Brian", "Data Engineering", "Mombasa")
introduce("Njeri", "Data Science", "Nairobi")
Name: Brian | Course: Data Engineering | From: Mombasa
Name: Njeri | Course: Data Science | From: Nairobi
return: The Dish That Comes Out
Here's where a lot of beginners get confused - and I was deliberate about this moment.
There's a big difference between a function that prints something and a function that returns something.
# This function prints - you can see the result, but can't USE it
def add_print(a, b):
print(a + b)
# This function returns - you get the result back to work with
def add_return(a, b):
return a + b
Watch what happens when you try to use each one:
result1 = add_print(3, 4) # prints 7
result2 = add_return(3, 4) # gives back 7
print(result1) # None — print() gives nothing back
print(result2) # 7 — return gives the value back
7
None
7
That None caused a proper reaction. "Why is it None?!"
Because print() is a one-way announcement - it shows something on screen and gives you nothing back. return hands the value back to whoever called the function, so you can store it, add to it, pass it to another function.
Rule of thumb I gave them: if you need to do something with the result later - use return. If you just need to display it - print() is fine.
Now the grade calculator from the teaser made full sense:
def calculate_grade(score):
if score >= 80:
return "A"
elif score >= 70:
return "B"
elif score >= 60:
return "C"
elif score >= 50:
return "D"
else:
return "F"
# Now we can USE the result
score = 74
grade = calculate_grade(score)
print(f"Score: {score} → Grade: {grade}")
# Or pass it directly into another function
print(f"Kamau's grade: {calculate_grade(88)}")
print(f"Aisha's grade: {calculate_grade(43)}")
Score: 74 → Grade: B
Kamau's grade: A
Aisha's grade: F
And just like that, Brian's three repeated grade blocks became one clean function called three times. His code got shorter. More importantly, it got trustworthy.
Scope: The Moment That Broke Everyone
I knew this was coming. It happens every cohort.
A student - Njeri this time - wrote a function, defined a variable inside it, then tried to use that variable outside the function. And got an error.
def calculate_total(price, quantity):
total = price * quantity # total is created inside the function
return total
calculate_total(250, 3)
print(total) # NameError - total doesn't exist out here
NameError: name 'total' is not defined
"But I just calculated it?!"
Yes , inside the function. Variables created inside a function live inside the function. When the function finishes, they disappear. This is called scope.
The analogy: a function is like a separate room. What happens in that room, stays in that room. If you want something to come out, you have to hand it through the door — which is what return does.
result = calculate_total(250, 3) # catch what comes through the door
print(result) # now you have it
750
Once the room analogy landed, scope clicked for everyone. A variable inside a function is local , it only exists in that room. A variable defined outside, at the top level, is global , accessible everywhere.
Default Arguments: When Some Ingredients Are Optional
Sometimes you want a function to have a sensible default behaviour, but still let the caller override it if they want.
def send_sms(recipient, message, sender="Lux Systems"):
print(f"To: {recipient}")
print(f"From: {sender}")
print(f"Message: {message}")
print("---")
# Use the default sender
send_sms("Amina", "Your results are ready.")
# Override the default
send_sms("Brian", "Staff meeting at 3pm.", sender="HR Department")
To: Amina
From: Lux Systems
Message: Your results are ready.
---
To: Brian
From: HR Department
Message: Staff meeting at 3pm.
---
One rule: default parameters always go at the end. You can't have a default parameter before a regular one - Python won't know which is which.
*args: Wait, Unlimited Arguments?
This one earned the best reaction of the session.
What if you don't know in advance how many values someone will pass to your function? *args handles it:
def calculate_total(*prices):
total = 0
for price in prices:
total += price
return total
print(calculate_total(50)) # one item
print(calculate_total(50, 120, 75)) # three items
print(calculate_total(200, 450, 30, 75, 90)) # five items
50
245
845
"Wait - the same function works for any number of inputs?!"
Yes. *args collects everything passed in and gives it to you as a tuple you can loop through. You don't have to know in advance how many there will be.
A practical example - a receipt generator that handles any number of items:
def print_receipt(customer, *items):
print(f"\n=== Receipt for {customer} ===")
total = 0
for item, price in items:
print(f" {item}: KES {price}")
total += price
print(f" ---------------")
print(f" TOTAL: KES {total}")
print_receipt(
"Wanjiku",
("Unga 2kg", 180),
("Sukari 1kg", 130),
("Mafuta 500ml", 95)
)
=== Receipt for Wanjiku ===
Unga 2kg: KES 180
Sukari 1kg: KES 130
Mafuta 500ml: KES 95
---------------
TOTAL: KES 405
We Built This Together: Student Report Generator
The session project — combining everything: parameters, return, default arguments, loops:
def calculate_average(scores):
return sum(scores) / len(scores)
def get_grade(average):
if average >= 80:
return "A"
elif average >= 70:
return "B"
elif average >= 60:
return "C"
elif average >= 50:
return "D"
else:
return "F"
def print_report(name, scores, school="Lux Academy"):
average = calculate_average(scores)
grade = get_grade(average)
print(f"\n{'='*35}")
print(f" {school}")
print(f" Student Report")
print(f"{'='*35}")
print(f" Name: {name}")
print(f" Scores: {scores}")
print(f" Average: {average:.1f}")
print(f" Grade: {grade}")
print(f"{'='*35}")
print_report("Kamau Njoroge", [78, 85, 90, 72, 88])
print_report("Aisha Mwangi", [55, 62, 48, 70, 59])
print_report("Baraka Odhiambo", [91, 88, 95, 97, 92], school="Greenwood Academy")
===================================
Lux Academy
Student Report
===================================
Name: Kamau Njoroge
Scores: [78, 85, 90, 72, 88]
Average: 82.6
Grade: A
===================================
===================================
Lux Academy
Student Report
===================================
Name: Aisha Mwangi
Scores: [55, 62, 48, 70, 59]
Average: 58.8
Grade: D
===================================
===================================
Greenwood Academy
Student Report
===================================
Name: Baraka Odhiambo
Scores: [91, 88, 95, 97, 92]
Average: 92.6
Grade: A
===================================
When this ran cleanly, the room went quiet for a second - then someone said "that looks like actual software."
That's the moment. Functions calling functions, clean output, reusable logic. That's not a tutorial exercise anymore. That's the beginning of how real programs are built.
Practice Problems
Easy:
# 1. Write a function that takes a name and prints a welcome message
# 2. Write a function that takes two numbers and returns the larger one
# 3. Write a function that takes a score and returns "Pass" or "Fail" (pass mark: 50)
Medium:
# M-Pesa transaction fee calculator
# Safaricom charges different fees based on amount sent (simplified):
# KES 1 – 100 → KES 0 (free)
# KES 101 – 500 → KES 7
# KES 501 – 1000 → KES 13
# KES 1001 – 5000 → KES 28
# Above 5000 → KES 52
def calculate_mpesa_fee(amount):
# Your code here
pass
print(calculate_mpesa_fee(250)) # should return 7
print(calculate_mpesa_fee(1500)) # should return 28
print(calculate_mpesa_fee(80)) # should return 0
Challenge:
# Build a complete BMI calculator with interpretation
# Formula: BMI = weight(kg) / height(m)²
# Underweight: BMI < 18.5
# Normal: 18.5 – 24.9
# Overweight: 25 – 29.9
# Obese: BMI >= 30
def calculate_bmi(weight, height):
pass
def interpret_bmi(bmi):
pass
def bmi_report(name, weight, height):
pass # Call both functions above and print a clean report
bmi_report("Njeri", 58, 1.65)
What I Noticed Teaching This Session
1. The DRY reveal works best with their own code. Showing a student their own repeated logic - not a made-up example - makes the problem feel real and personal. It's not abstract. It's their code that needs fixing.
2. print vs return is the most important distinction in this session. Don't rush it. The None moment - where a student expects a value and gets nothing - is the best teacher. Engineer it deliberately.
3. Scope clicks fastest with a physical analogy. The "separate room" analogy for local scope worked better than any code-first explanation. Give the analogy, then show the code, then show the error. In that order.
4. Functions calling functions is a mindset shift. When students saw print_report call calculate_average and get_grade internally, something shifted. Programs stopped being a list of instructions and started feeling like a system of parts. That's the beginning of thinking like a software developer.
What's Coming in Week 5: Data Structures
So far every variable we've worked with holds one thing. One name. One score. One balance.
Week 5 changes that entirely:
# A list - ordered, changeable
scores = [78, 45, 92, 61, 88]
# A dictionary - look things up by name
student = {
"name": "Amina",
"course": "Data Engineering",
"score": 92
}
print(student["name"]) # Amina
print(scores[2]) # 92
One variable. Multiple values. And when you combine these with the functions we just learned - you're building real data structures. That's next week.
Try It Yourself
- Download Python 3
- Download VS Code
- This week's code on GitHub ← link coming soon
Start with the M-Pesa fee calculator — it's a real-world function with clear inputs, clear outputs, and conditions you can verify. If it passes all three test cases, you've got functions figured out.
I'm a data trainer in Nairobi running a full data programme -
Python foundations → Data Science or Data Engineering specialisations.
I write weekly about what we covered, what worked, and what surprised me.
Follow along or drop your questions in the comments.
Top comments (0)