Programming with Python | Chapter 8: Functions – Defining and Calling

Chapter Objectives

  • Understand the purpose of functions for code reusability and organization (abstraction) in Python.
  • Learn how to define a function using the def keyword.
  • Understand how to call (execute) a defined function.
  • Differentiate between parameters (in function definition) and arguments (in function call).
  • Use positional and keyword arguments when calling functions.
  • Define functions with default parameter values.
  • Understand how functions return values using the return statement (and implicit None return).
  • Write docstrings to document functions.
  • Grasp the basics of variable scope (local vs. global).

Introduction

As programs grow larger, simply writing code sequentially becomes difficult to manage and prone to errors. Repeating the same block of code in multiple places is inefficient. Functions are named blocks of reusable code designed to perform a specific task. By defining functions, we can break down complex problems into smaller, manageable pieces (decomposition), avoid repetition (DRY – Don’t Repeat Yourself), and make our code easier to read, test, and maintain. This chapter covers how to define your own functions, pass data into them using parameters, get results back using return values, and understand how variables behave within functions (scope).

Theory & Explanation

What are Functions?

Think of a function like a mini-program within your main program. It has a name, it can accept inputs (called arguments), it performs a specific sequence of operations, and it can optionally produce an output (called a return value).

We’ve already used built-in functions like print(), len(), input(), type(), int(), str(), range(), and methods like .append(), .lower(), .items(). Now, we learn to create our own.

Benefits of Using Functions:

  • Reusability: Write code once, call it multiple times from different parts of your program.
  • Organization: Break down large programs into logical, self-contained units.
  • Abstraction: Hide complex implementation details behind a simple function call. The user of the function only needs to know what it does, not necessarily how it does it.
  • Readability: Well-named functions make code easier to understand.
  • Maintainability: Changes or bug fixes only need to be made in one place (the function definition).
  • Testability: Functions can be tested independently.

Defining a Function (def)

You define a function using the def keyword, followed by the function name, parentheses (), and a colon :. The code block that belongs to the function must be indented.

Syntax:

Python
def function_name(parameter1, parameter2, ...):
    """Optional docstring explaining what the function does."""
    # Indented code block (function body)
    statement1
    statement2
    # ...
    return value # Optional return statement
  • def: Keyword indicating a function definition.
  • function_name: Follows the same naming rules as variables (snake_case recommended). Should be descriptive of the function’s purpose.
  • (parameter1, parameter2, ...): Optional list of parameters – variables that will receive input values when the function is called. If the function doesn’t take input, use empty parentheses ().
  • """Docstring""": An optional string literal (usually triple-quoted) right after the def line, used to document the function’s purpose, parameters, and return value. Highly recommended!
  • Indented Code Block: The actual code the function executes.
  • return value: Optional statement to send a result back from the function. If omitted, the function implicitly returns None.
Python
# A simple function definition
def greet():
    """Prints a simple greeting."""
    print("Hello there!")

# A function with parameters
def greet_user(name):
    """Prints a personalized greeting."""
    print(f"Hello, {name}!")

# A function with parameters and a return value
def add_numbers(num1, num2):
    """Adds two numbers and returns the result."""
    result = num1 + num2
    return result

Calling a Function

Defining a function doesn’t execute its code. To run the code inside a function, you need to call it by using its name followed by parentheses (), providing any required arguments inside the parentheses.

Python
# Calling the functions defined above
greet() # Output: Hello there!

greet_user("Alice") # Output: Hello, Alice!

sum_result = add_numbers(5, 3) # Call add_numbers, store the returned value
print(f"The sum is: {sum_result}") # Output: The sum is: 8

print(add_numbers(10, -2)) # Output: 8 (Can print the returned value directly)

graph TD
    A[Start Program] --> B("Call function_name(args)");
    B -- Pass Arguments --> C{Enter Function Body};
    C --> D[Execute Statements];
    D --> E{Reach 'return value'?};
    E -- Yes --> F[Exit Function, Return 'value'];
    E -- No (End of Body) --> G[Exit Function, Return None];
    F -- Returned Value --> H(Resume after function call);
    G -- Returned None --> H;
    H --> I[Continue Program];

    style C fill:#ccf,stroke:#33a
    style F fill:#cfc,stroke:#080
    style G fill:#fcc,stroke:#a33

Parameters vs. Arguments

These terms are often confused but have distinct meanings:

  • Parameters: Variables listed inside the parentheses in the function definition. They are placeholders for the values the function expects to receive. (e.g., name, num1, num2 in the definitions above).
  • Arguments: The actual values passed into the function when it is called. (e.g., "Alice", 5, 3 in the calls above).

When you call a function, the arguments you provide are assigned to the corresponding parameters in the function definition.

Positional and Keyword Arguments

There are two main ways to pass arguments to a function:

Positional Arguments: Arguments are matched to parameters based on their position. The first argument goes to the first parameter, the second to the second, and so on. The order matters.

Python
def describe_pet(animal_type, pet_name):
    """Displays information about a pet."""
    print(f"I have a {animal_type}.")
    print(f"My {animal_type}'s name is {pet_name.capitalize()}.")

# Using positional arguments (order matters)
describe_pet("hamster", "harry")
# Output:
# I have a hamster.
# My hamster's name is Harry.

Keyword Arguments: Arguments are specified using the parameter name followed by an equals sign (=) and the value (parameter_name=value). The order of keyword arguments doesn’t matter, and they can be mixed with positional arguments (but positional arguments must come before keyword arguments).

Python
# Using keyword arguments (order doesn't matter)
describe_pet(pet_name="willie", animal_type="dog")
# Output:
# I have a dog.
# My dog's name is Willie.

# Mixing positional and keyword (positional first)
describe_pet("cat", pet_name="whiskers")
# Output:
# I have a cat.
# My cat's name is Whiskers.

# describe_pet(pet_name="lucy", "dog") # SyntaxError: positional argument follows keyword argument

Default Parameter Values

You can provide default values for parameters in the function definition. If an argument for that parameter is not provided during the function call, the default value is used. Parameters with default values must come after parameters without default values.

Python
def power(base, exponent=2): # exponent defaults to 2 if not provided
    """Calculates base raised to the power of exponent."""
    return base ** exponent

print(power(5))      # No exponent argument provided, uses default 2. Output: 25
print(power(5, 3))   # Exponent argument provided (3). Output: 125
print(power(base=3)) # Using keyword argument, exponent defaults to 2. Output: 9

Return Values (return)

The return statement is used to send a value back from the function to the point where it was called.

  • A function can return any type of value (number, string, list, dictionary, tuple, boolean, None, even another function).
  • When a return statement is executed, the function terminates immediately, and execution resumes at the calling point.
  • A function can have multiple return statements (e.g., inside if/else blocks), but only one will be executed per call.
  • If a function reaches the end of its block without encountering a return statement, it implicitly returns the special value None.
Python
def format_name(first_name, last_name):
    """Returns a neatly formatted full name."""
    if not first_name or not last_name:
        return None # Return None if either name is empty/missing
    full_name = f"{first_name.capitalize()} {last_name.capitalize()}"
    return full_name

name1 = format_name("ada", "lovelace")
print(name1) # Output: Ada Lovelace

name2 = format_name("", "hopper")
print(name2) # Output: None

def print_status(message):
    """Prints a status message. Doesn't explicitly return anything."""
    print(f"Status: {message}")

result = print_status("Processing complete.") # Output: Status: Processing complete.
print(result) # Output: None (because print_status implicitly returns None)

Docstrings

A docstring (documentation string) is a string literal placed as the very first statement in a module, function, class, or method definition. It’s used to explain what the code does. Triple quotes ("""Docstring goes here""") are typically used to allow for multi-line descriptions.

Good docstrings explain:

  • The function’s purpose.
  • What arguments it takes (Args: section).
  • What it returns (Returns: section).
  • Any errors it might raise (Raises: section, less common for basic functions).
Python
def calculate_area(length, width):
    """Calculates the area of a rectangle.

    Args:
        length (float or int): The length of the rectangle.
        width (float or int): The width of the rectangle.

    Returns:
        float or int: The calculated area of the rectangle.
        Returns None if either dimension is non-positive.
    """
    if length <= 0 or width <= 0:
        return None
    return length * width

# You can access the docstring using help() or the __doc__ attribute
help(calculate_area)
# print(calculate_area.__doc__)

Writing good docstrings is crucial for making your code understandable to others (and your future self!).

Variable Scope

Scope refers to the region of your program where a variable is accessible.

  • Local Scope: Variables defined inside a function (including parameters) are local to that function. They only exist while the function is executing and cannot be accessed from outside the function.
  • Global Scope: Variables defined outside of any function (at the top level of your script) have global scope. They can be accessed (read) from anywhere, including inside functions.
Python
global_var = "I am global"

def my_function():
    local_var = "I am local"
    print(f"Inside function: {local_var}")
    print(f"Inside function, accessing global: {global_var}")

my_function()
# Output:
# Inside function: I am local
# Inside function, accessing global: I am global

print(f"Outside function: {global_var}") # Works fine
# print(f"Outside function: {local_var}") # NameError: name 'local_var' is not defined

graph TD
    %% Global Scope
    subgraph GlobalScope["Global Scope"]
        direction LR
        G1[global_var = 10]
        G2("Call my_func()")
        G3["print(global_var)"]
        G1 --> G2 --> G3
    end

    %% Function Scope
    subgraph FunctionScope["my_func() Scope"]
        direction TB
        F1[local_var = 5] --> F2["print(local_var)"]
        F2 --> F3["print(global_var)"]
    end

    %% Flow into and out of function
    G2 -- "Enters Function" --> F1
    F3 -- "Exits Function" --> G3

    %% Uncomment to illustrate access error
    %% G3 --> Error[print(local_var) -> NameError]

    %% Styling
    style GlobalScope fill:#e6f3ff,stroke:#0066cc
    style FunctionScope fill:#fff0e6,stroke:#ff8000

Modifying Globals (Generally Avoid): While you can modify a global variable from within a function using the global keyword, it’s generally considered bad practice as it makes code harder to understand and debug. It’s usually better to pass values into functions via parameters and get results back via return statements.

Python
count = 0 # Global

def increment_bad():
    global count # Declare intent to modify the global 'count'
    count += 1
    print(f"Inside (bad): {count}")

def increment_good(current_count):
    # Takes current count as input, returns the new count
    return current_count + 1

increment_bad() # Modifies global count directly
print(f"Outside (after bad): {count}")

count = increment_good(count) # Gets new value via return, reassigns global
print(f"Outside (after good): {count}")

Code Examples

Example 1: Simple Function for Reusability

Python
# reusable_functions.py

def calculate_circle_area(radius):
    """Calculates the area of a circle given its radius."""
    if radius < 0:
        return None # Area cannot be negative
    pi = 3.14159
    return pi * (radius ** 2)

def calculate_circle_circumference(radius):
    """Calculates the circumference of a circle given its radius."""
    if radius < 0:
        return None
    pi = 3.14159
    return 2 * pi * radius

# --- Main part of the script ---
radius1 = 5
area1 = calculate_circle_area(radius1)
circumference1 = calculate_circle_circumference(radius1)

if area1 is not None:
    print(f"Circle with radius {radius1}:")
    print(f"- Area: {area1:.2f}") # Format to 2 decimal places
    print(f"- Circumference: {circumference1:.2f}")
else:
    print(f"Invalid radius: {radius1}")

radius2 = -2
area2 = calculate_circle_area(radius2)
if area2 is not None:
     print(f"Area for radius {radius2}: {area2:.2f}")
else:
    print(f"\nInvalid radius: {radius2}")

Explanation:

  • We define two functions, calculate_circle_area and calculate_circle_circumference, each performing a specific calculation. This avoids repeating the formulas and the value of pi.
  • Both functions include basic validation (checking for negative radius) and return None if the input is invalid.
  • The main part of the script calls these functions with different radii and handles the potential None return value.

Example 2: Function with Default Values and Keyword Arguments

Python
# message_formatter.py

def format_message(text, sender="System", priority="Normal", timestamp=None):
    """Formats a message string with sender, priority, and optional timestamp.

    Args:
        text (str): The main content of the message.
        sender (str, optional): The sender's name. Defaults to "System".
        priority (str, optional): The message priority. Defaults to "Normal".
        timestamp (str, optional): An optional timestamp string. Defaults to None.

    Returns:
        str: The formatted message string.
    """
    formatted = f"[{priority}] From: {sender}\n"
    if timestamp:
        formatted += f"Time: {timestamp}\n"
    formatted += f"Message: {text}"
    return formatted

# --- Using the function ---

# Only required argument
msg1 = format_message("Server rebooting.")
print(msg1 + "\n" + "-"*20)

# Providing some optional arguments positionally (must be in order)
msg2 = format_message("User login failed.", "AuthService", "High")
print(msg2 + "\n" + "-"*20)

# Providing optional arguments using keywords (order doesn't matter)
import datetime
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") # Get current time string

msg3 = format_message(text="Backup complete.", priority="Low", timestamp=now)
print(msg3 + "\n" + "-"*20)

# Mixing positional and keyword (positional first)
msg4 = format_message("Disk space low!", priority="Critical", sender="Monitor")
print(msg4 + "\n" + "-"*20)

Explanation:

  • The format_message function has one required parameter (text) and three optional parameters (sender, priority, timestamp) with default values.
  • The function body constructs a formatted string, including the timestamp only if it’s provided (not None).
  • The examples show different ways to call the function: using only the required argument, using positional arguments for the first few optional ones, using keyword arguments for specific optional ones, and mixing positional and keyword arguments.
graph TD
    subgraph Caller Scope
        A["result = add(5, 3)"] -- Argument 1 (5) --> P1(num1);
        A -- Argument 2 (3) --> P2(num2);
    end

    subgraph Function Definition ["add(num1, num2)"]
        P1 --> Body[Function Body Uses num1];
        P2 --> Body;
        Body --> R{return num1 + num2};
        R -- Return Value (8) --> A;
    end

    style P1 fill:#f9f,stroke:#333
    style P2 fill:#f9f,stroke:#333

Common Mistakes or Pitfalls

  • Forgetting Parentheses: Calling a function without parentheses (my_function instead of my_function()) doesn’t execute the function; it usually just refers to the function object itself.
  • Mismatching Arguments/Parameters: Providing too few or too many arguments compared to the function’s parameters (unless default values are used).
  • Scope Confusion: Trying to access a local variable outside its function, or unintentionally modifying global variables without using the global keyword (which is generally discouraged anyway).
  • Ignoring Return Values: Calling a function that returns a useful value but not assigning the result to a variable or using it directly, effectively discarding the result.
  • Implicit None Return: Forgetting that a function returns None if it doesn’t have an explicit return statement, which can lead to errors if you expect a different type of value.
  • Default Value Gotcha (Mutable Defaults): Using mutable objects (like lists or dictionaries) as default parameter values can lead to unexpected behavior because the default object is created only once when the function is defined, not each time it’s called. This is a more advanced topic but worth noting.

Chapter Summary

  • Functions are named, reusable blocks of code defined using def.
  • They promote code reusability, organization, abstraction, and maintainability.
  • Functions are executed by calling them: function_name(arguments).
  • Parameters are variables in the function definition; arguments are the values passed during the call.
  • Arguments can be positional (matched by order) or keyword (name=value, order doesn’t matter, must follow positional).
  • Parameters can have default values, making the corresponding arguments optional.
  • The return statement sends a value back from the function; functions return None implicitly if return is omitted.
  • Docstrings ("""...""") are used to document functions.
  • Variables defined inside functions have local scope; variables defined outside have global scope. Local variables cannot be accessed globally.

Function Concepts Summary

Concept Syntax / Example Description
Definition def my_func(param1, param2):
    """Docstring"""
    # Function body
    return value
Creates a named block of reusable code using the def keyword.
Calling result = my_func(arg1, arg2) Executes the code inside the function definition, passing arguments.
Parameters def my_func(param1, param2): Variables listed in the function definition that act as placeholders for inputs.
Arguments my_func(arg1, arg2) The actual values passed into a function when it is called.
Positional Args my_func(10, 20) Arguments matched to parameters based on their order (10 -> param1, 20 -> param2).
Keyword Args my_func(param2=20, param1=10) Arguments specified by parameter name (name=value). Order doesn’t matter. Must follow positional arguments if mixed.
Default Values def my_func(p1, p2=100): Assigns a default value to a parameter in the definition. The argument becomes optional during the call.
Return Value def my_func(a, b):
    return a + b
The return statement sends a value back from the function to the caller. Execution stops.
Implicit Return def my_func():
    print("Hi")
If a function ends without an explicit return, it automatically returns the special value None.
Docstring def my_func():
    """This function does X..."""
A string literal (usually triple-quoted) as the first line inside a definition, used for documentation. Accessed via help() or __doc__.
Local Scope def my_func():
    local_var = 5
Variables defined inside a function (including parameters) only exist and are accessible within that function during its execution.
Global Scope global_var = 10
def my_func():
    print(global_var)
Variables defined outside any function can be accessed (read) from anywhere, including inside functions.

Functions help organize code, improve reusability, and manage complexity.

Exercises & Mini Projects

Code

def add_numbers(num1, num2):     “””Adds two numbers.”””     result = num1 + num2     return result sum_val = add_numbers(5, 3) print(f”Sum is: {sum_val}”)

Execution Trace

Initial state. Press ‘Run’.

Exercises

  1. Simple Greeter: Define a function say_hello(name) that takes a name as an argument and prints a greeting like “Hello, [Name]!”. Call the function with your name.
  2. Rectangle Area (Function): Rewrite the rectangle area calculation from a previous chapter’s exercise. Define a function calculate_rectangle_area(width, height) that takes width and height and returns the calculated area. Call the function with some values and print the returned result.
  3. Default Argument: Define a function greet_with_message(name, message="Welcome!") that prints a message including the name and the message. Call it once with just a name (using the default message) and once with both a name and a custom message.
  4. Return Check: Define a function is_even(number) that takes an integer and returns True if the number is even, and False otherwise. Call the function with an even and an odd number and print the results.
  5. Docstring Practice: Copy the is_even function from Exercise 4 and add a proper docstring explaining what it does, its parameter, and what it returns. Use help(is_even) to view your docstring.

Mini Project: Calculator Refactored

Goal: Refactor the Simple Calculator Mini Project from Chapter 4 to use functions.

Steps:

  1. Define separate functions for each arithmetic operation:
    • add(num1, num2): Returns the sum.
    • subtract(num1, num2): Returns the difference.
    • multiply(num1, num2): Returns the product.
    • divide(num1, num2): Returns the result of division. This function should handle the “division by zero” case (e.g., by returning None or printing an error and returning None).
  2. Add docstrings to each of these functions.
  3. In the main part of your script (where you get user input for numbers and the operation symbol):
    • Keep the input logic for num1, operation, and num2.
    • Instead of performing the calculation directly in the if-elif-else block, call the appropriate function based on the operation symbol.
    • Store the result returned by the function in a variable.
    • Check if the result is None (for the division-by-zero case). If not None, print the result. If it is None, an error message should have already been printed by the divide function (or you can print one here).
  4. Consider putting the main input and if-elif-else logic inside its own function, e.g., run_calculator(), and then call run_calculator() at the end of the script. This further organizes the code.

Additional Sources:

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top