At some point every beginner writes the same five lines of code for the third time and thinks, there has to be a better way. There is: a function, a named block of code you write once and run whenever you need it. This post covers defining and calling functions, parameters and arguments, return values, and scope, then shows where they landed in a real project.
Your first function
You create a function with def, give it a name, and indent the code that belongs to it. Nothing runs until you call it by name.
def greet():
print("Hello, welcome to Python!")
greet()
greet()
The function was written once and ran twice. If the greeting ever changes, you fix it in one place instead of hunting down every copy.
Passing things in
Parameters let you pass information in. In def greet(name), name is the parameter, the placeholder. The value you pass when calling, like "Amina", is the argument.
def greet(name):
print(f"Hello, {name}!")
greet("Amina")
Parameters can have default values, and you can pass arguments by name. Parameters with defaults have to come after the ones without.
def describe_bundle(name, price, validity="24 hours"):
print(f"{name} costs Ksh {price} and is valid for {validity}")
describe_bundle("50MB", 5)
describe_bundle("500MB", 50, "7 days")
describe_bundle(name="2GB", price=200, validity="30 days")
50MB costs Ksh 5 and is valid for 24 hours
500MB costs Ksh 50 and is valid for 7 days
2GB costs Ksh 200 and is valid for 30 days
Getting an answer back
Printing shows something on the screen, but it hands nothing back to the rest of your program. return does.
def calculate_total(price, quantity):
return price * quantity
total = calculate_total(50, 3)
print(total) # 150
If a function only prints, its result is None, which is why a value that keeps coming back as None usually means you printed where you meant to return. A function can also return several values at once:
def min_max(numbers):
return min(numbers), max(numbers)
lowest, highest = min_max([4, 9, 1, 7])
print(lowest, highest) # 1 9
Where a variable lives
Variables created inside a function only exist inside it. Once the function finishes, they're gone. Passing a value in doesn't change the original either, because the function works on its own copy:
def deduct(balance, amount):
balance = balance - amount
return balance
balance = 200
deduct(balance, 50)
print(balance) # 200, unchanged
balance = deduct(balance, 50)
print(balance) # 150
The first call did the maths but nothing caught the answer. The second stored the returned value back into balance, and that's what actually updated it. So if the new value matters, return it and reassign it.
Putting it to work: a bill calculator
def calculate_bill(price, quantity, discount_rate=0):
subtotal = price * quantity
discount = subtotal * discount_rate
return subtotal - discount
print(f"Ksh {calculate_bill(100, 3):.2f}") # Ksh 300.00
print(f"Ksh {calculate_bill(100, 3, 0.1):.2f}") # Ksh 270.00
Same function, two situations, and the discount is optional.
How I used functions in my Bundle Purchase Simulator
I used all of this in my Bundle Purchase Simulator, a menu-driven program inspired by dialling *544#. Instead of one giant block, each job got its own function: main_menu() shows the menu, show_bundles() prints a category, buy_bundle() handles a purchase, and check_balance() reports the balance.
def check_balance(balance):
print(f"Your current balance is Ksh {balance:.2f}")
The part that made scope and return values click was the balance. buy_bundle() deducts money from it, but a function can't change the outer variable directly. So it returns the new balance, and the main loop catches it:
if choice == "1":
current_balance = buy_bundle("Data", bundles["Data"], current_balance)
Without that reassignment, every purchase would look successful and the balance would never move. Splitting the program into functions also meant I could test each one on its own before wiring them together, which made bugs far easier to find.
Top comments (0)