9 Functions & Modularity
In This Chapter
As your programs get bigger, they can get messy. Yes, loops help us stop repeating ourselves, but what if we need to use a specific chunk of code in Level 1, and then need that exact same code again in Level 5?
To build complex software, we have to break our code down into smaller, reusable tools. In this chapter, we will learn how to architect real software. We will cover:
- Defining Functions: How to write your own mini-programs.
- The Call Stack: How the computer jumps around your code.
- Parameters & Arguments: Passing data into your functions.
- Returns &
None: Pushing data out, and what happens when you forget. - Variable Scope & Memory: The invisible walls that protect your data.
- The Function Design Recipe: How professionals plan their code.
- The Project: Refactoring our Dark Cave adventure into a clean architecture.
9.1 Introduction: Building Your Own Tools
In previous chapters, you used built-in functions that Python provides for you, like print(), input(), and len(). You didn’t have to write the complex code required to count items in a list or draw letters on a screen; you just typed the function name, and the computer did the work.
A function is simply a named mini-program inside your main program.
Programmers write their own functions to organize their code into logical blocks. This is called Modularity. Instead of writing one massive script, you build a bunch of smaller tools, and your main script just tells those tools when to run.
9.2 Defining a Function
To create our own function, we use the def keyword (short for “define”).
Open a new File Editor window and type this exact code:
Run the script. The output will look like this:
Starting the game...
Hello, brave adventurer!
Game over.The Call Stack (Jumping Around)
Notice the order of the output. The first thing printed was Starting the game..., even though say_hello() was at the very top of the file!
When the computer sees the def keyword, it reads the code inside, but it does not run it. It just memorizes it. The computer doesn’t actually execute the mini-program until you call it by typing say_hello().
Here is exactly what the computer’s brain did:
- It memorized the
say_helloinstructions. - It printed
"Starting the game...". - It saw the
say_hello()call. It placed a bookmark here, jumped up to the function, and ran the two print statements inside it. - When the function finished, it jumped back down to its bookmark and resumed the main script, printing
"Game over."
9.3 Passing Data: Parameters and Arguments
A tool isn’t very useful if it can only do the exact same thing every time. If you build a calculate_damage() function, it needs to know what weapon you are holding. We have to pass data into the function.
When we define the function, we can reserve a place for data to be passed in. This reserved space is a variable called a Parameter. When we actually call the function, the specific data we hand to it is called an Argument.
# 'weapon_power' is the Parameter.
# We can do operations on it like any other variable.
def calculate_damage(weapon_power):
total = weapon_power * 2
print(f"You dealt {total} damage!")
# When we call the function 'calculate_damage'
# We pass the Argument '10' in.
# It will take the reserved 'weapon_power' spot.
calculate_damage(10)The Memory Model in Functions
Let’s trace what happens in the computer’s memory when we call calculate_damage(10).
- The computer creates the integer object
10in memory (let’s say ataddr1). - It jumps up to the function. It takes the parameter name tag,
weapon_power, and attaches it toaddr1. - Now, inside the function, whenever the computer sees
weapon_damage, it follows the arrow to10.
So the parameter weapon_power is like a variable we can use in our function and know that it will have a value later when it is called.
9.4 Pushing Data Out: return
Usually, we don’t want a function to just print() an answer; we want it to hand the answer back to the main script so we can use it in math or store it in a variable. We can pass data back OUT of the function using the return keyword.
def calculate_damage(weapon_power):
total = weapon_power * 2
return total # This sends the answer back out!
# We pass the integer 10 IN, and catch the returned answer OUT in a new variable
my_damage = calculate_damage(10)When Python hits the return keyword, the function is instantly destroyed, and the value takes its place in the main script.
Omitting a Return Statement: None
What happens if you set a variable equal to a function, but you forget to include a return statement inside that function?
Python has a special data type called None. It literally means “nothing.” If a function finishes running and doesn’t hit a return keyword, Python automatically returns None by default.
def forgetful_function():
math = 2 + 2
# Oops! We forgot to return the answer!
result = forgetful_function()
print(result)
# Output: NoneIf you ever see None printing in your game when you expected a number or a string, check your functions! You probably forgot your return statement.
9.5 Variable Scope: The Invisible Walls
When you create a variable inside a function, it is temporary. It belongs only to that function. The rest of your program cannot see it. This is called Local Scope. Think of a function like a soundproof room. The main script cannot see or hear what is happening inside the room.
Let’s look at a script that crashes because of Scope. Do not type this, just read it:
def craft_sword():
weapon_name = "Steel Sword" # This is a Local variable
craft_sword()
# The main script tries to print the variable...
print(weapon_name) CRASH! NameError: name 'weapon_name' is not defined.
Why? Because of how the Memory Model handles functions. When craft_sword() is called, Python creates a temporary workspace. It creates the "Steel Sword" object and attaches the weapon_name tag to it.
But the moment the function finishes running, Python destroys the temporary workspace. The weapon_name tag is thrown in the trash. The main script has absolutely no idea what weapon_name is!
If a function needs to give data to the main script, you must use return to push the data out of the soundproof room before it gets destroyed.
9.6 The Function Design Recipe
A careful programmer does not just start typing code and hope it works. They plan their functions using a specific recipe. This ensures the function is easy to read, handles errors, and does exactly what it is supposed to do.
Type Annotations (The Contract): We can and should annotate our functions to tell other programmers what data types go IN, and what data types come OUT. We use a -> to point to the return type.
The Docstring (The Description): Right below the def line, we use triple-quotes
"""to write a multi-line comment explaining exactly what the function does.
Here is what a well described function might look like in code:
def multiply_numbers(num1: int, num2: int) -> int:
"""
Takes two integers, multiplies them together,
and returns the total.
"""
total = num1 * num2
return totalIf you type help(multiply_numbers) in the interactive shell later, Python will actually print your Docstring to the screen to remind you how your own tool works!
9.7 The Project: Revisiting the Dark Cave
We are going to return to our old friend, the Dark Caves game from Chapter 5. We will break it down into three clean functions: show_intro(), get_player_choice(), and resolve_action(). We will also use a while loop (from Chapter 4) to force the player to type a valid choice!
Type this out entirely, save it as dark_cave_modular.py, and run it.
# --- 1. FUNCTION DEFINITIONS ---
def show_intro():
print("Welcome to the Dark Cave.")
print("You stand in a damp stone room. Two tunnels lead deeper into the earth.")
def get_player_choice():
choice = ""
# This loop waits for the player to enter a valid choice.
# If its not "left" or "right", it will keep asking.
while choice != "left" and choice != "right":
choice = input("Do you go 'left' or 'right'? ").strip().lower()
# Push the valid choice out of the function
return choice
def resolve_action(player_choice):
# This function expects an argument (left or right)
if player_choice == "left":
print("\nYou walk down the left tunnel. It is a dead end.")
print("But you find a shiny steel sword! You win!")
elif player_choice == "right":
print("\nYou walk down the right tunnel. It is very dark.")
print("A giant spider drops from the ceiling! GAME OVER.")
# --- 2. MAIN SCRIPT ---
# The computer skips the definitions above and starts actually running the game here!
show_intro()
# We call the choice function, and catch its returned answer in a variable
move = get_player_choice()
# We pass that choice IN to the action function
resolve_action(move)Breaking Down the Execution
Even though this file is longer than the Chapter 3 version, it is much better. Let’s track the computer’s brain:
- It reads the three
defblocks, memorizes them, and then moves on. - It hits
show_intro(). It jumps up, prints the two lines of text, and jumps back down. - It hits
move = get_player_choice(). It jumps up toget_player_choice(). It traps the player in awhileloop until they type “left” or “right”. When they do, itreturnsthe word “left” or “right”. The function dies, and the word is stored in themovevariable. - It hits
resolve_action(move). It jumps up, taking the word “left” or “right” with it. It checks theifstatement, prints the text, and the program ends.
By breaking the game into three functions, we have made it much easier to read and understand. Each function has a single job, and the main script is now a clean, readable story of how the game flows. It will now be much easier to add new features, without having to rewrite the entire game from scratch.
Challenge: The Infinite Caves
Your game is now modular, which makes adding features incredibly easy. Right now, the game ends after one choice. Let’s turn it into an infinite loop so the player can keep playing until they get eaten by the spider!
The Goal: Wrap the main script in a while loop so the game repeats until the player chooses the wrong tunnel.
Step-by-Step Guide:
- Look at your
resolve_action(player_choice)function. Right now, it just prints text. Modify it so that if the player goes left, itreturns True(meaning they survived). If they go right, itreturns False(meaning they died). - Go to your Main Script section at the bottom. Delete everything except
show_intro(). - Under
show_intro(), create a variable calledis_alive = True. - Create a
while is_alive == True:loop. - Indent the last two lines (
move = ...andresolve_action(...)) so they are inside the loop. - The final puzzle: How do you catch the
TrueorFalsecoming out ofresolve_action()to update youris_alivevariable? (Hint: Look at how you caught the answer fromget_player_choice()!)