Chapter 7: Functions — Reusable Blocks of Code
You've been writing programs that run top to bottom — one long sequence of instructions. That works for small programs. But as programs grow, that approach falls apart fast.
Imagine you need to calculate a discount in five different places in your program. Without functions, you copy the same four lines five times. Then the discount formula changes — and you have to find and fix all five copies. Miss one, and your program gives wrong answers.
Functions solve this. You write the logic once, give it a name, and call that name wherever you need it. Change the function once, and every place that calls it gets the update automatically.
That's the idea. But functions do much more than just avoid repetition — they let you break a complex problem into small, named, solvable pieces. Every professional programmer thinks this way.
What is a Function?
A function is a named block of code that you define once and can run (call) as many times as you want.
You've already used functions — print(), input(), len(), range(), int(), round(). Those are built-in functions that Python provides. Now you'll write your own.
In plain English: A function is like a recipe. You write it once. Whenever you want that dish, you follow the recipe — you don't rewrite it from scratch.
Defining Your First Function
Use the def keyword, then the function name, then parentheses, then a colon. The code inside the function is indented.
def say_hello():
print("Hello!")
print("Nice to meet you.")
say_hello()
say_hello()
say_hello()
Output:
Hello!
Nice to meet you.
Hello!
Nice to meet you.
Hello!
Nice to meet you.
The function runs every time you call it. You wrote the two print lines once, but ran them three times.
Important: You must define a function before you call it. If you call it before the def, Python raises a NameError.
say_hello() # NameError — Python doesn't know this yet
def say_hello():
print("Hello!")
Always define first, call second.
Your turn: Write a function called print_divider that prints a line of 30 dashes (- x 30). Call it three times. You'll use this kind of function to format output in real programs.
Parameters — Passing Information In
A function that always does the exact same thing has limited use. Parameters let you pass information in so the function can work with different data each time.
def greet(name):
print(f"Hello, {name}! Welcome.")
greet("Alice")
greet("Bob")
greet("Carlos")
Output:
Hello, Alice! Welcome.
Hello, Bob! Welcome.
Hello, Carlos! Welcome.
name is the parameter — a placeholder in the function definition. "Alice", "Bob", "Carlos" are the arguments — the actual values you pass in when you call the function.
You can have multiple parameters:
def describe_person(name, age, city):
print(f"{name} is {age} years old and lives in {city}.")
describe_person("Diana", 29, "Lagos")
describe_person("Kenji", 41, "Tokyo")
Output:
Diana is 29 years old and lives in Lagos.
Kenji is 41 years old and lives in Tokyo.
By default, you must pass arguments in the same order as the parameters. describe_person("Diana", 29, "Lagos") assigns "Diana" to name, 29 to age, "Lagos" to city.
Your turn: Write a function called calculate_area that takes length and width as parameters and prints the area (length x width). Call it with three different pairs of values.
Return Values — Getting Information Back
So far our functions print results. But printing and returning are two very different things.
- Printing shows a value on screen. The rest of your program can't use it.
- Returning sends a value back to wherever the function was called. Your program can store it, calculate with it, pass it to another function.
def add(a, b):
return a + b
result = add(10, 5)
print(result) # 15
print(add(3, 7) * 2) # 20
The return statement does two things: it sends a value back, and it immediately exits the function. Any code after return in the same block never runs.
def check_positive(number):
if number > 0:
return True
return False
print(check_positive(5)) # True
print(check_positive(-3)) # False
A function that returns something is far more useful than one that just prints, because you can use the result in calculations, conditions, lists, or anywhere else.
def celsius_to_fahrenheit(c):
return (c * 9/5) + 32
temps_celsius = [0, 20, 37, 100]
for temp in temps_celsius:
f = celsius_to_fahrenheit(temp)
print(f"{temp}°C = {f}°F")
Output:
0°C = 32.0°F
20°C = 68.0°F
37°C = 98.6°F
100°C = 212.0°F
What if a function has no return statement? It implicitly returns None — Python's way of saying "nothing."
def say_hi():
print("Hi!")
result = say_hi()
print(result) # None
Your turn: Write a function called calculate_discount that takes a price and a discount_percent, and returns the final price after the discount. Test it with a few prices and discounts, storing and printing the results.
Default Parameter Values
Sometimes you want a parameter to have a sensible default — a value it uses when the caller doesn't provide one.
def greet(name, greeting="Hello"):
print(f"{greeting}, {name}!")
greet("Alice") # uses default greeting
greet("Bob", "Good morning") # overrides the default
greet("Carlos", "¡Hola")
Output:
Hello, Alice!
Good morning, Bob!
¡Hola, Carlos!
Parameters with defaults must come after parameters without defaults.
def make_coffee(size, strength="medium", milk=False):
print(f"{size} coffee, strength: {strength}, milk: {milk}")
make_coffee("large")
make_coffee("small", "strong")
make_coffee("medium", "light", True)
Output:
large coffee, strength: medium, milk: False
small coffee, strength: strong, milk: False
medium coffee, strength: light, milk: True
Your turn: Write a function called power that takes a base and an exponent (default 2). It should return base ** exponent. Call it with just a base (it should square it), and then with both a base and an exponent.
Keyword Arguments
When calling a function, you can name the arguments explicitly. This makes the call more readable and lets you pass them in any order.
def describe_book(title, author, year):
print(f'"{title}" by {author} ({year})')
# Positional — order matters
describe_book("Dune", "Frank Herbert", 1965)
# Keyword — order doesn't matter
describe_book(author="George Orwell", year=1949, title="1984")
Output:
"Dune" by Frank Herbert (1965)
"1984" by George Orwell (1949)
You can mix positional and keyword arguments, but positional arguments must come first.
describe_book("Brave New World", year=1932, author="Aldous Huxley")
*args — Any Number of Arguments
What if you don't know how many arguments the caller will pass? Use *args. It collects all extra positional arguments into a tuple.
def total(*args):
print(f"Arguments received: {args}")
return sum(args)
print(total(1, 2, 3)) # 6
print(total(10, 20, 30, 40)) # 100
print(total(5)) # 5
Output:
Arguments received: (1, 2, 3)
Arguments received: (10, 20, 30, 40)
Arguments received: (5,)
6
100
5
Inside the function, args is a tuple containing all the values passed in. You can loop over it, pass it to sum(), or do anything you'd do with a regular tuple.
def greet_all(*names):
for name in names:
print(f"Hello, {name}!")
greet_all("Alice", "Bob", "Carlos", "Diana")
Output:
Hello, Alice!
Hello, Bob!
Hello, Carlos!
Hello, Diana!
**kwargs — Any Number of Keyword Arguments
**kwargs collects extra keyword arguments into a dictionary.
def show_profile(**kwargs):
for key, value in kwargs.items():
print(f" {key}: {value}")
show_profile(name="Alice", age=30, city="Paris", job="Engineer")
Output:
name: Alice
age: 30
city: Paris
job: Engineer
Inside the function, kwargs is a regular dictionary. You can use any keyword arguments the caller wants to pass — you don't have to define them in advance.
You can combine all four: regular parameters, defaults, *args, and **kwargs.
def log_event(level, *messages, **metadata):
print(f"[{level}]", " ".join(messages))
for key, value in metadata.items():
print(f" {key}={value}")
log_event("INFO", "User logged in", user="alice", ip="192.168.1.1")
Output:
[INFO] User logged in
user=alice
ip=192.168.1.1
Scope — Where Variables Live
Every variable in Python has a scope — the part of the program where it exists and can be accessed.
Local scope
Variables created inside a function exist only inside that function. They're destroyed when the function returns.
def my_function():
x = 10 # local variable
print(x)
my_function() # 10
print(x) # NameError — x doesn't exist here
Global scope
Variables created outside any function are in the global scope. They exist for the entire program.
name = "Alice" # global variable
def greet():
print(f"Hello, {name}!") # can READ a global variable
greet() # Hello, Alice!
A function can read a global variable. But if you try to assign to it inside the function, Python creates a new local variable with that name instead — the global is untouched.
count = 0
def increment():
count = count + 1 # UnboundLocalError! Python thinks count is local
The global keyword
To modify a global variable from inside a function, declare it with global.
count = 0
def increment():
global count
count = count + 1
increment()
increment()
increment()
print(count) # 3
Use global sparingly. In most cases, a better design is to pass the value in as a parameter and return the updated value — that keeps functions self-contained and predictable.
# Better approach — no global needed
def increment(count):
return count + 1
count = 0
count = increment(count)
count = increment(count)
count = increment(count)
print(count) # 3
Docstrings — Documenting Your Functions
A docstring is a string at the very start of a function that explains what it does. Write it with triple quotes.
def celsius_to_fahrenheit(celsius):
"""
Convert a temperature from Celsius to Fahrenheit.
Parameters:
celsius (float): Temperature in Celsius.
Returns:
float: Temperature in Fahrenheit.
"""
return (celsius * 9/5) + 32
Access a function's docstring with help() or .__doc__:
help(celsius_to_fahrenheit)
print(celsius_to_fahrenheit.__doc__)
Good docstrings answer three questions: what does this function do, what does it take as input, and what does it return? Write them for any function someone else (or future you) will use.
Calling Functions from Functions
Functions can call other functions. This is how you build complex behavior from simple pieces.
def square(n):
return n ** 2
def sum_of_squares(a, b):
return square(a) + square(b)
def hypotenuse(a, b):
"""Return the hypotenuse of a right triangle with sides a and b."""
return sum_of_squares(a, b) ** 0.5
print(hypotenuse(3, 4)) # 5.0
print(hypotenuse(5, 12)) # 13.0
Each function does one small thing. Combined, they calculate the hypotenuse. This is called decomposition — breaking a problem into small, focused functions. It's one of the most important skills in programming.
Returning Multiple Values
A Python function can return more than one value. Separate them with commas and Python returns them as a tuple.
def min_max(numbers):
"""Return both the minimum and maximum of a list."""
return min(numbers), max(numbers)
scores = [88, 72, 95, 61, 84]
lowest, highest = min_max(scores)
print(f"Lowest: {lowest}") # 61
print(f"Highest: {highest}") # 95
The lowest, highest = min_max(scores) line unpacks the tuple into two variables in one step. You'll see this pattern everywhere in real Python code.
Putting It All Together: A BMI Calculator
def calculate_bmi(weight_kg, height_m):
"""
Calculate Body Mass Index (BMI).
Parameters:
weight_kg (float): Weight in kilograms.
height_m (float): Height in metres.
Returns:
float: BMI rounded to 1 decimal place.
"""
return round(weight_kg / height_m ** 2, 1)
def bmi_category(bmi):
"""Return the BMI category as a string."""
if bmi < 18.5:
return "Underweight"
elif bmi < 25:
return "Normal weight"
elif bmi < 30:
return "Overweight"
else:
return "Obese"
def print_bmi_report(name, weight_kg, height_m):
"""Print a complete BMI report for one person."""
bmi = calculate_bmi(weight_kg, height_m)
category = bmi_category(bmi)
print(f"Name: {name}")
print(f"BMI: {bmi}")
print(f"Category: {category}")
print()
print_bmi_report("Alice", 65, 1.70)
print_bmi_report("Bob", 90, 1.75)
print_bmi_report("Carlos", 55, 1.80)
Output:
Name: Alice
BMI: 22.5
Category: Normal weight
Name: Bob
BMI: 29.4
Category: Overweight
Name: Carlos
BMI: 17.0
Category: Underweight
Three small functions, each doing one job. print_bmi_report calls both of the others. The result is clean, readable, and easy to test or change.
What You Learned in This Chapter
defdefines a function. Call it by writing its name with parentheses.- Parameters are placeholders in the definition. Arguments are the values you pass when calling.
returnsends a value back. A function with noreturnreturnsNone.- Default parameters give a fallback value when the caller doesn't provide one.
- Keyword arguments let you pass values by name, in any order.
*argscollects any number of positional arguments into a tuple.**kwargscollects any number of keyword arguments into a dictionary.- Scope: local variables exist only inside their function. Global variables exist everywhere but should be modified with
globalonly when necessary. - Docstrings document what a function does, what it takes, and what it returns.
- Functions can call other functions — break big problems into small pieces.
- A function can return multiple values, unpacked with
a, b = function().
What's Next?
Your programs can now store collections of data (lists) and organize logic into reusable functions. But everything disappears when the program stops. If you run your grade tracker, enter 30 students, and close the terminal — all that data is gone.
In Chapter 8, you'll learn how to read and write files, so your data can survive after the program ends. You'll save results to a file, read them back later, and start building programs that remember things between runs.
Your turn: Write a function called summarize that takes a list of numbers and returns a dictionary with four keys: "count", "total", "average", and "max". Then write a second function called print_summary that takes that dictionary and prints each value neatly. Call both functions with a list of your choice.