19. Defining and Calling Functions
19.1) What Functions Are and Why They Matter
A function is a named block of code that performs a specific task. You've already been using functions throughout this book—print(), input(), len(), type(), and many others. These are built-in functions that Python provides. Now you'll learn to create your own custom functions to organize your code and make it reusable.
Why Functions Matter
Functions are fundamental to writing clear, maintainable programs. They provide several critical benefits:
1. Code Reusability
Without functions, you'd need to copy and paste the same code every time you want to perform a task. Consider calculating the area of a rectangle in multiple places:
# Without functions - repetitive code
length1 = 5
width1 = 3
area1 = length1 * width1
print(f"Area 1: {area1}")
length2 = 8
width2 = 4
area2 = length2 * width2
print(f"Area 2: {area2}")
length3 = 10
width3 = 6
area3 = length3 * width3
print(f"Area 3: {area3}")Output:
Area 1: 15
Area 2: 32
Area 3: 60This repetition is tedious and error-prone. If you need to change how you calculate area (perhaps to include units or rounding), you'd have to update every location. Functions solve this problem by letting you write the code once and use it many times.
2. Code Organization
Functions break large programs into smaller, manageable pieces. Each function handles one specific task, making your code easier to understand and maintain. Instead of one long script with hundreds of lines, you can organize related operations into named functions that clearly communicate their purpose.
3. Abstraction
Functions hide implementation details behind a simple interface. When you call len(my_list), you don't need to know how Python counts the elements—you just get the result. Similarly, your functions can provide simple interfaces to complex operations, making your code easier to use and understand.
4. Testing and Debugging
Functions make it easier to test individual pieces of your program. You can verify that each function works correctly in isolation before combining them into a larger program. When something goes wrong, functions help you narrow down where the problem is occurring.
Throughout the rest of this chapter, you'll learn how to define your own functions, pass information to them, get results back, and document them clearly. These skills are essential for writing professional Python code.
19.2) Defining Functions with def
To create a function in Python, you use the def keyword (short for "define"). The basic structure of a function definition looks like this:
def function_name():
# Code block that runs when function is called
statement1
statement2
# ... more statementsLet's break down each part:
def: The keyword that tells Python you're defining a functionfunction_name: The name you choose for your function (follows the same rules as variable names)(): Parentheses that will eventually hold parameters (we'll cover those in the next section):: A colon that marks the end of the function header- Indented code block: The statements that make up the function body (must be indented)
Your First Function
Here's a simple function that prints a greeting:
def greet():
print("Hello!")
print("Welcome to Python functions.")
# Call the function
greet()Output:
Hello!
Welcome to Python functions.When you define a function, Python remembers it but doesn't execute the code inside immediately. The code only runs when you call the function by writing its name followed by parentheses: greet().
Function Naming Conventions
Function names follow the same rules as variable names (as we learned in Chapter 3):
- Use lowercase letters
- Separate words with underscores (snake_case)
- Start with a letter or underscore, not a digit
- Use descriptive names that indicate what the function does
# Good function names
def calculate_total():
pass
def get_user_age():
pass
def display_menu():
pass
# Poor function names (but syntactically valid)
def x(): # Not descriptive
pass
def CalculateTotal(): # Should use lowercase
pass
def calc(): # Too abbreviated
passNote: We use pass here as a placeholder (as we learned in Chapter 8). It does nothing but allows the function definition to be syntactically complete.
Functions Can Contain Any Code
The body of a function can contain any Python statements you've learned so far: variable assignments, conditionals, loops, and even calls to other functions.
def check_temperature():
temperature = 72
if temperature > 75:
print("It's warm.")
elif temperature > 60:
print("It's comfortable.")
else:
print("It's cool.")
check_temperature()Output:
It's comfortable.Multiple Function Definitions
You can define as many functions as you need in your program. Each function is independent and can be called separately:
def morning_greeting():
print("Good morning!")
def evening_greeting():
print("Good evening!")
# Call each function
morning_greeting()
evening_greeting()Output:
Good morning!
Good evening!Function Definition Order
In Python, you must define a function before you call it. The Python interpreter reads your code from top to bottom, so if you try to call a function before defining it, you'll get an error:
# WARNING: This will cause a NameError - for demonstration only
# PROBLEM: Function called before it's defined
say_hello() # NameError: name 'say_hello' is not defined
def say_hello():
print("Hello!")The correct order is to define first, then call:
# Correct: Define first
def say_hello():
print("Hello!")
# Then call
say_hello()Output:
Hello!However, functions can call other functions that are defined later in the file, as long as those calls happen after all the definitions:
def first_function():
print("First function")
second_function() # This is fine - called at runtime
def second_function():
print("Second function")
# Both functions are defined before we call the first one
first_function()Output:
First function
Second functionFunctions Create Local Scope
Variables created inside a function exist only within that function. This is called local scope (we'll explore this in detail in Chapter 21). For now, understand that what happens inside a function stays inside the function:
def create_message():
message = "This is local"
print(message)
create_message()
# This would cause an error:
# print(message) # NameError: name 'message' is not definedOutput:
This is localThe variable message exists only while the function is running. Once the function finishes, the variable disappears.
Empty Functions with pass
Sometimes you want to define a function structure but implement it later. Use pass as a placeholder:
def future_feature():
pass # TODO: Implement this later
# The function exists and can be called, but does nothing
future_feature() # Runs without error, does nothingThis is useful when sketching out your program's structure before filling in the details.
19.3) Calling Functions and Passing Arguments
Defining a function creates a reusable piece of code, but to make functions truly powerful, you need to pass information to them. This information is passed through arguments.
Parameters vs Arguments
Before we continue, let's clarify two terms that are often confused:
- Parameter: A variable name in the function definition that will receive a value
- Argument: The actual value you pass to the function when you call it
def greet(name): # 'name' is a parameter
print(f"Hello, {name}!")
greet("Alice") # "Alice" is an argumentOutput:
Hello, Alice!Think of parameters as placeholders and arguments as the actual data that fills those placeholders.
Defining Functions with Parameters
To define a function that accepts input, add parameter names inside the parentheses:
def greet_person(name):
print(f"Hello, {name}!")
print("Nice to meet you.")
# Call with different arguments
greet_person("Alice")
print() # Blank line for readability
greet_person("Bob")Output:
Hello, Alice!
Nice to meet you.
Hello, Bob!
Nice to meet you.The parameter name acts as a variable inside the function. Each time you call the function, name takes on the value of the argument you provide.
Multiple Parameters
Functions can accept multiple parameters, separated by commas:
def calculate_rectangle_area(length, width):
area = length * width
print(f"A rectangle with length {length} and width {width}")
print(f"has an area of {area} square units.")
calculate_rectangle_area(5, 3)
print()
calculate_rectangle_area(10, 7)Output:
A rectangle with length 5 and width 3
has an area of 15 square units.
A rectangle with length 10 and width 7
has an area of 70 square units.When calling a function with multiple parameters, the order matters. The first argument goes to the first parameter, the second argument to the second parameter, and so on. These are called positional arguments.
Positional Arguments
With positional arguments, Python matches arguments to parameters based on their position:
def describe_pet(animal_type, pet_name):
print(f"I have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name}.")
describe_pet("dog", "Buddy")
print()
describe_pet("cat", "Whiskers")Output:
I have a dog.
My dog's name is Buddy.
I have a cat.
My cat's name is Whiskers.If you mix up the order, you'll get unexpected results:
def describe_pet(animal_type, pet_name):
print(f"I have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name}.")
# Arguments in wrong order
describe_pet("Buddy", "dog")Output:
I have a Buddy.
My Buddy's name is dog.This is technically valid Python, but it produces nonsensical output because the arguments are in the wrong positions.
Keyword Arguments
To avoid position-related errors, you can use keyword arguments by explicitly naming the parameters when you call the function:
def describe_pet(animal_type, pet_name):
print(f"I have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name}.")
# Using keyword arguments - order doesn't matter
describe_pet(animal_type="dog", pet_name="Buddy")
print()
describe_pet(pet_name="Whiskers", animal_type="cat")Output:
I have a dog.
My dog's name is Buddy.
I have a cat.
My cat's name is Whiskers.With keyword arguments, the order doesn't matter because Python matches arguments to parameters by name, not position.
Mixing Positional and Keyword Arguments
You can mix positional and keyword arguments in a single function call, but positional arguments must come first:
def create_profile(username, email, age):
print(f"Username: {username}")
print(f"Email: {email}")
print(f"Age: {age}")
# Mixing positional and keyword arguments
create_profile("alice123", email="alice@example.com", age=25)Output:
Username: alice123
Email: alice@example.com
Age: 25However, you cannot put positional arguments after keyword arguments:
# WARNING: This will cause a SyntaxError - for demonstration only
# PROBLEM: Positional argument after keyword argument
# create_profile(username="alice123", "alice@example.com", 25)
# SyntaxError: positional argument follows keyword argumentArgument Count Must Match
When you call a function, you must provide the correct number of arguments (unless the function has default values, which we'll cover in Chapter 20):
def add_numbers(a, b):
result = a + b
print(f"{a} + {b} = {result}")
add_numbers(5, 3) # Correct: 2 arguments for 2 parametersOutput:
5 + 3 = 8Providing too few or too many arguments causes an error:
# WARNING: These will cause TypeErrors - for demonstration only
# PROBLEM: Too few arguments
# add_numbers(5)
# TypeError: add_numbers() missing 1 required positional argument: 'b'
# PROBLEM: Too many arguments
# add_numbers(5, 3, 2)
# TypeError: add_numbers() takes 2 positional arguments but 3 were givenUsing Expressions as Arguments
Arguments don't have to be simple values—you can use any expression:
def display_total(price, quantity):
total = price * quantity
print(f"Total cost: ${total:.2f}")
# Using expressions as arguments
base_price = 10
display_total(base_price * 1.1, 5) # Price with 10% markup
display_total(15 + 5, 3 * 2) # Both arguments are expressionsOutput:
Total cost: $55.00
Total cost: $120.00Python evaluates each expression first, then passes the resulting values to the function.
Calling Functions from Within Functions
Functions can call other functions, creating a hierarchy of operations. This is a powerful technique that lets you break complex tasks into smaller, manageable pieces.
Here's an example with room area calculation:
def calculate_area(length, width):
return length * width
def display_room_info(room_name, length, width):
area = calculate_area(length, width)
print(f"Room: {room_name}")
print(f"Dimensions: {length} x {width}")
print(f"Area: {area} square feet")
display_room_info("Living Room", 15, 12)Output:
Room: Living Room
Dimensions: 15 x 12
Area: 180 square feetNote: We're using return here, which we'll explore in detail in the next section. For now, understand that calculate_area() sends its result back to the calling function.
Here's another example showing how functions can build on each other—a temperature conversion system:
def celsius_to_fahrenheit(celsius):
return (celsius * 9/5) + 32
def format_temperature(fahrenheit):
return f"{fahrenheit:.1f}°F"
def display_temperature_conversion(celsius):
fahrenheit = celsius_to_fahrenheit(celsius)
formatted = format_temperature(fahrenheit)
print(f"{celsius}°C equals {formatted}")
# Use the complete conversion system
display_temperature_conversion(25)
display_temperature_conversion(0)
display_temperature_conversion(100)Output:
25°C equals 77.0°F
0°C equals 32.0°F
100°C equals 212.0°FIn this example, display_temperature_conversion() calls celsius_to_fahrenheit() to do the conversion, then calls format_temperature() to format the result. Each function has a single, clear responsibility, making the code easy to understand and maintain.
19.4) Using return to Send Back Results
So far, our functions have performed actions (like printing), but they haven't sent values back to the code that called them. The return statement lets a function compute a result and send it back to the caller.
Basic return Statement
Here's a simple function that calculates and returns a value:
def add_numbers(a, b):
result = a + b
return result
# Capture the returned value
sum_value = add_numbers(5, 3)
print(f"The sum is: {sum_value}")Output:
The sum is: 8When Python encounters a return statement, two things happen:
- The function immediately stops executing (any code after
returnis ignored) - The specified value is sent back to the caller
Returning Values Directly
You don't need to store the result in a variable before returning it. You can return an expression directly:
def multiply(a, b):
return a * b
result = multiply(4, 7)
print(f"4 × 7 = {result}")Output:
4 × 7 = 28This is more concise and is the preferred style for simple calculations.
Using Returned Values
Once a function returns a value, you can use that value anywhere you'd use any other value:
def calculate_discount(price, discount_percent):
discount_amount = price * (discount_percent / 100)
return discount_amount
original_price = 100
discount = calculate_discount(original_price, 20)
# Use the returned value in calculations
final_price = original_price - discount
print(f"Original price: ${original_price:.2f}")
print(f"Discount: ${discount:.2f}")
print(f"Final price: ${final_price:.2f}")Output:
Original price: $100.00
Discount: $20.00
Final price: $80.00return Exits the Function Immediately
When Python executes a return statement, the function stops immediately. Any code after the return is never executed:
def check_age(age):
if age < 18:
return "Minor"
# This line only runs if age >= 18
return "Adult"
print(check_age(15))
print(check_age(25))Output:
Minor
AdultThis behavior is useful for handling different cases in a function. Once you've determined the result, you can return immediately without needing to check additional conditions.
Here's an example that demonstrates how return stops execution:
def process_number(n):
if n < 0:
return "Negative"
print("This line runs for non-negative numbers")
if n == 0:
return "Zero"
print("This line runs for positive numbers")
return "Positive"
print(process_number(-5))
print()
print(process_number(0))
print()
print(process_number(10))Output:
Negative
This line runs for non-negative numbers
Zero
This line runs for non-negative numbers
This line runs for positive numbers
PositiveFunctions Without return
If a function doesn't have a return statement, or if it has a return with no value, the function returns None:
def greet(name):
print(f"Hello, {name}!")
# No return statement
result = greet("Alice")
print(f"The function returned: {result}")Output:
Hello, Alice!
The function returned: NoneSimilarly, a bare return (with no value) also returns None:
def process_data(data):
if not data:
return # Early exit, returns None
print(f"Processing: {data}")
return "Success"
result1 = process_data("")
result2 = process_data("some data")
print(f"Result 1: {result1}")
print(f"Result 2: {result2}")Output:
Processing: some data
Result 1: None
Result 2: SuccessReturning Multiple Values
Python functions can return multiple values by separating them with commas. Python automatically packs them into a tuple (as we learned in Chapter 15):
def calculate_rectangle(length, width):
area = length * width
perimeter = 2 * (length + width)
return area, perimeter
# Unpack the returned tuple
rect_area, rect_perimeter = calculate_rectangle(5, 3)
print(f"Area: {rect_area}")
print(f"Perimeter: {rect_perimeter}")Output:
Area: 15
Perimeter: 16You can also capture the tuple as a single value:
def get_student_info():
name = "Alice"
age = 20
grade = "A"
return name, age, grade
# Capture as a tuple
student = get_student_info()
print(f"Student info: {student}")
print(f"Name: {student[0]}")Output:
Student info: ('Alice', 20, 'A')
Name: AliceReturning Different Types
A function can return different types of values depending on the situation:
def divide(a, b):
if b == 0:
return "Error: Division by zero"
return a / b
result1 = divide(10, 2)
result2 = divide(10, 0)
print(f"10 / 2 = {result1}")
print(f"10 / 0 = {result2}")Output:
10 / 2 = 5.0
10 / 0 = Error: Division by zeroWhile this works, it's generally better practice to handle errors differently (we'll learn about exceptions in Part VII). For now, understand that functions can return different types, though it's often clearer to be consistent.
19.5) Documenting Functions with Docstrings
As your programs grow and you create more functions, it becomes crucial to document what each function does. Python provides a built-in way to document functions using docstrings (documentation strings).
What Is a Docstring?
A docstring is a string literal that appears as the first statement in a function (or module, class, or method). It describes what the function does, what parameters it accepts, and what it returns. Docstrings are enclosed in triple quotes (""" or '''), which allow them to span multiple lines.
def calculate_area(length, width):
"""Calculate the area of a rectangle.
Takes the length and width of a rectangle and returns the area.
"""
return length * widthWhy Docstrings Matter
Docstrings serve several important purposes:
- Self-Documentation: They explain what your function does without requiring readers to analyze the code
- IDE Support: Many development tools display docstrings as tooltips when you use a function
- help() Function: Python's built-in
help()function displays docstrings - Professional Practice: Well-documented code is easier to maintain and share with others
Basic Docstring Format
For simple functions, a one-line docstring is sufficient:
def greet(name):
"""Print a personalized greeting."""
print(f"Hello, {name}!")
# Access the docstring
print(greet.__doc__)Output:
Print a personalized greeting.The docstring should be a concise description of what the function does, written as a command ("Calculate...", "Return...", "Print...") rather than a description ("This function calculates...").
Multi-Line Docstrings
For more complex functions, use multi-line docstrings that include:
- A brief summary on the first line
- A blank line
- More detailed description
- Information about parameters
- Information about return values
def calculate_discount(price, discount_percent):
"""Calculate the discounted price.
Takes an original price and a discount percentage, then returns
the amount of discount that should be applied.
Parameters:
price (float): The original price before discount
discount_percent (float): The discount percentage (0-100)
Returns:
float: The discount amount in the same currency as the price
"""
return price * (discount_percent / 100)
# Use help() to see the full docstring
help(calculate_discount)Output:
Help on function calculate_discount in module __main__:
calculate_discount(price, discount_percent)
Calculate the discounted price.
Takes an original price and a discount percentage, then returns
the amount of discount that should be applied.
Parameters:
price (float): The original price before discount
discount_percent (float): The discount percentage (0-100)
Returns:
float: The discount amount in the same currency as the priceDocstring Conventions
Python has established conventions for writing docstrings (documented in PEP 257). Here are the key guidelines:
1. Use triple double-quotes: """docstring"""
def good_example():
"""This follows the convention."""
pass
def also_valid():
'''This works but is less common.'''
pass2. One-line docstrings should fit on one line:
def add(a, b):
"""Return the sum of a and b."""
return a + b3. Multi-line docstrings should have a summary line, then a blank line:
def process_order(order_id, items):
"""Process a customer order and update inventory.
This function validates the order, checks inventory availability,
calculates the total cost, and updates the inventory database.
Parameters:
order_id (str): Unique identifier for the order
items (list): List of item dictionaries with 'product' and 'quantity'
Returns:
dict: Order summary with 'total', 'status', and 'confirmation_number'
"""
# Function implementation here
passDescribing Parameters and Return Values
When documenting parameters and return values, be specific about:
- Parameter names: Match the actual parameter names in the function
- Types: What type of data is expected (we'll learn about type hints in Chapter 43)
- Purpose: What the parameter is used for
- Return value: What the function returns and under what conditions
def find_student(student_id, students):
"""Find a student by ID in a list of student records.
Parameters:
student_id (int): The unique ID number of the student to find
students (list): List of student dictionaries, each containing 'id' and 'name'
Returns:
dict: The student dictionary if found, None if not found
"""
for student in students:
if student['id'] == student_id:
return student
return NoneDocstrings for Functions with Multiple Return Types
When a function can return different types depending on the situation, document all possibilities:
def safe_divide(a, b):
"""Divide two numbers with error handling.
Parameters:
a (float): The dividend
b (float): The divisor
Returns:
float: The quotient if division is successful
str: An error message if b is zero
"""
if b == 0:
return "Error: Cannot divide by zero"
return a / bAccessing Docstrings
You can access a function's docstring in three ways:
1. Using the __doc__ attribute:
def example():
"""This is an example function."""
pass
print(example.__doc__)Output:
This is an example function.2. Using the help() function:
def calculate_bmi(weight, height):
"""Calculate Body Mass Index.
Parameters:
weight (float): Weight in kilograms
height (float): Height in meters
Returns:
float: BMI value
"""
return weight / (height ** 2)
help(calculate_bmi)Output:
Help on function calculate_bmi in module __main__:
calculate_bmi(weight, height)
Calculate Body Mass Index.
Parameters:
weight (float): Weight in kilograms
height (float): Height in meters
Returns:
float: BMI value3. In interactive development environments: Most IDEs and code editors display docstrings as tooltips when you hover over or type a function name.
When to Write Docstrings
You should write docstrings for:
- All public functions: Functions that are meant to be used by other parts of your program or by other programmers
- Complex functions: Any function whose purpose or behavior isn't immediately obvious from its name and parameters
- Functions with non-obvious parameters: When parameter names alone don't fully explain what values are expected
You might skip docstrings for:
- Very simple, obvious functions: Functions like
def add(a, b): return a + bwhere the name and parameters make the purpose crystal clear - Private helper functions: Small internal functions used only within a larger function (though even these benefit from brief docstrings)
Docstrings Are Not Comments
Remember that docstrings serve a different purpose than comments:
- Docstrings: Describe what a function does and how to use it (the interface)
- Comments: Explain how the code works internally (the implementation)
def calculate_grade(score, total):
"""Calculate the percentage grade from a score.
Parameters:
score (int): Points earned
total (int): Total points possible
Returns:
float: The percentage grade (0-100)
"""
# Avoid division by zero
if total == 0:
return 0.0
# Calculate percentage and round to 2 decimal places
percentage = (score / total) * 100
return round(percentage, 2)The docstring tells users what the function does and how to use it. The comments explain specific implementation details to someone reading the code.
Building Good Documentation Habits
Writing clear docstrings is a habit that pays dividends:
- Write docstrings as you write functions: Don't wait until later—document while the function's purpose is fresh in your mind
- Keep docstrings updated: When you change a function's behavior, update its docstring
- Be concise but complete: Include all necessary information, but avoid unnecessary verbosity
- Use examples when helpful: For complex functions, a usage example in the docstring can be invaluable
Good documentation makes your code more professional, easier to maintain, and more valuable to others (including your future self).