Python & AI Tutorials Logo
Python Programming

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:

python
# 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: 60

This 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:

python
def function_name():
    # Code block that runs when function is called
    statement1
    statement2
    # ... more statements

Let's break down each part:

  • def: The keyword that tells Python you're defining a function
  • function_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:

python
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
python
# 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
    pass

Note: 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.

python
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:

python
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:

python
# 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:

python
# 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:

python
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 function

Functions 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:

python
def create_message():
    message = "This is local"
    print(message)
 
create_message()
 
# This would cause an error:
# print(message)  # NameError: name 'message' is not defined

Output:

This is local

The 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:

python
def future_feature():
    pass  # TODO: Implement this later
 
# The function exists and can be called, but does nothing
future_feature()  # Runs without error, does nothing

This 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
python
def greet(name):  # 'name' is a parameter
    print(f"Hello, {name}!")
 
greet("Alice")  # "Alice" is an argument

Output:

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:

python
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:

python
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:

python
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:

python
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:

python
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:

python
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: 25

However, you cannot put positional arguments after keyword arguments:

python
# 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 argument

Argument 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):

python
def add_numbers(a, b):
    result = a + b
    print(f"{a} + {b} = {result}")
 
add_numbers(5, 3)  # Correct: 2 arguments for 2 parameters

Output:

5 + 3 = 8

Providing too few or too many arguments causes an error:

python
# 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 given

Using Expressions as Arguments

Arguments don't have to be simple values—you can use any expression:

python
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 expressions

Output:

Total cost: $55.00
Total cost: $120.00

Python 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:

python
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 feet

Note: 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:

python
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°F

In 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:

python
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: 8

When Python encounters a return statement, two things happen:

  1. The function immediately stops executing (any code after return is ignored)
  2. 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:

python
def multiply(a, b):
    return a * b
 
result = multiply(4, 7)
print(f"4 × 7 = {result}")

Output:

4 × 7 = 28

This 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:

python
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.00

return Exits the Function Immediately

When Python executes a return statement, the function stops immediately. Any code after the return is never executed:

python
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
Adult

This 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:

python
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
Positive

Functions Without return

If a function doesn't have a return statement, or if it has a return with no value, the function returns None:

python
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: None

Similarly, a bare return (with no value) also returns None:

python
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: Success

Returning 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):

python
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: 16

You can also capture the tuple as a single value:

python
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: Alice

Returning Different Types

A function can return different types of values depending on the situation:

python
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 zero

While 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.

python
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 * width

Why Docstrings Matter

Docstrings serve several important purposes:

  1. Self-Documentation: They explain what your function does without requiring readers to analyze the code
  2. IDE Support: Many development tools display docstrings as tooltips when you use a function
  3. help() Function: Python's built-in help() function displays docstrings
  4. 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:

python
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
python
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 price

Docstring Conventions

Python has established conventions for writing docstrings (documented in PEP 257). Here are the key guidelines:

1. Use triple double-quotes: """docstring"""

python
def good_example():
    """This follows the convention."""
    pass
 
def also_valid():
    '''This works but is less common.'''
    pass

2. One-line docstrings should fit on one line:

python
def add(a, b):
    """Return the sum of a and b."""
    return a + b

3. Multi-line docstrings should have a summary line, then a blank line:

python
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
    pass

Describing 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
python
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 None

Docstrings for Functions with Multiple Return Types

When a function can return different types depending on the situation, document all possibilities:

python
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 / b

Accessing Docstrings

You can access a function's docstring in three ways:

1. Using the __doc__ attribute:

python
def example():
    """This is an example function."""
    pass
 
print(example.__doc__)

Output:

This is an example function.

2. Using the help() function:

python
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 value

3. 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 + b where 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)
python
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).

© 2025. Primesoft Co., Ltd.
support@primesoft.ai