10xdev Book: Programming from Zero to Pro with Python

Chapter 6: Functions in Python: Building Reusable Code Blocks

Chapter Introduction

Okay, your programs can now make decisions and repeat themselves. That’s a huge leap! But as you write more code, you’ll start noticing something: you’re often writing the same (or very similar) blocks of code over and over again in different places.

This repetition is a major red flag in programming. It makes your code longer, harder to read, and a real pain to update (fix a bug in one place? You have to find and fix it everywhere you copied it!). This leads us to a core principle: DRY – Don’t Repeat Yourself.

Functions are Python’s primary tool for staying DRY. A function is simply a named block of code designed to perform a specific task. You write the code once, give it a name, and then you can call that name whenever you need that task done, potentially with different inputs. Think of it as creating your own custom command or a reusable recipe. 🧑‍🍳


1. What is a Function and How Do We Define One?

To create (or define) a function, you use the def keyword.

Basic Structure:

def function_name(parameter1, parameter2): # Parameters are optional inputs
    """
    Optional docstring: Explains what the function does.
    Good practice!
    """
    # Indented code block: The function's actions
    print(f"Executing function_name with {parameter1} and {parameter2}")
    result = parameter1 + parameter2 # Example action
    return result # Optional: Send a value back
  • def: Tells Python you’re defining a function.
  • function_name: Your chosen name (use snake_case).
  • (): Parentheses are required. They hold parameters (input variables) if the function needs them.
  • :: The colon marks the start of the indented function body.
  • Docstring ("""..."""): A string literal on the first line explaining the function. Crucial for documentation and help tools.
  • Function Body: The indented code that runs when the function is called.
  • return: (Optional) Sends a value back to the place where the function was called.

Calling a Function

Defining a function doesn’t run it. You need to call it by using its name followed by parentheses, providing any required arguments (the actual values for the parameters).

def greet(name: str): # Added a type hint for clarity
    """Prints a simple greeting."""
    print(f"Hello, {name}!")

# Calling the function with the argument "Alice"
greet("Alice")
greet("Bob") # Call it again with a different argument

2. Inputs: Parameters vs. Arguments

These terms are often confused:

  • Parameter: The variable name inside the function definition’s parentheses (e.g., name in def greet(name):). It’s the placeholder for the input.
  • Argument: The actual value you pass in when you call the function (e.g., "Alice" in greet("Alice")). It’s the data that fills the placeholder.

3. Outputs: print vs. return

This difference is critical:

  • print(): Displays output to the user on the console. The function itself doesn’t produce a value that your code can use later.
  • return: Sends a value back to the code that called the function. This allows you to store the result, use it in calculations, or pass it to another function.
def add_numbers(num1: float, num2: float) -> float: # Hinting input and output types
    """Adds two numbers and returns the sum."""
    calculation = num1 + num2
    return calculation # Send the result back

# Call the function and store the returned value
sum_result = add_numbers(10.5, 5.0)

print(f"The print function just displays: {sum_result}")
# We can use the returned value in further operations
print(f"Result multiplied by 2: {sum_result * 2}")

# A function without a return statement implicitly returns None
def print_greeting(name: str):
    print(f"Hi, {name}!")

result_of_print = print_greeting("Charlie")
print(f"The print_greeting function returned: {result_of_print}") # Output: None

Functions that return a value are generally more reusable and flexible than those that only print.


4. Flexible Arguments

Default Parameter Values

You can make parameters optional by providing a default value in the def statement.

def power(base: float, exponent: float = 2.0) -> float: # exponent defaults to 2
    """Calculates base raised to the exponent power."""
    return base ** exponent

print(power(5))      # Uses default exponent 2 -> 25.0
print(power(5, 3))   # Overrides default, uses exponent 3 -> 125.0

Keyword Arguments

When calling a function, you can specify arguments by parameter name. This makes the order irrelevant and improves readability, especially for functions with many parameters.

def describe_pet(animal_type: str, pet_name: str):
    print(f"I have a {animal_type} named {pet_name}.")

describe_pet(pet_name="Whiskers", animal_type="cat") # Order doesn't matter here

5. Variable Scope: Local vs. Global

Where can you access a variable? That’s determined by its scope.

  • Local Scope: Variables created inside a function (including parameters) exist only within that function. They are local and disappear once the function finishes. This prevents functions from accidentally interfering with each other.

    def my_func():
        local_var = 100 # Local variable
        print(f"Inside function: {local_var}")
    
    my_func()
    # print(local_var) # This would cause a NameError! local_var doesn't exist here.
    
  • Global Scope: Variables created outside any function exist in the global scope and can generally be read from anywhere (including inside functions).

    global_var = "I'm global"
    
    def read_global():
        print(f"Inside function, reading global: {global_var}")
    
    read_global()
    print(f"Outside function: {global_var}")
    

Best Practice: Avoid modifying global variables from inside functions if possible (using the global keyword makes code harder to understand and debug). Pass data into functions via parameters and get results back via return.


6. Applied Project: An Organized Calculator (Refined)

Let’s revisit our calculator, making it cleaner using functions that return values.

# --- Organized Calculator with Functions ---

# Function definitions (the tools)
def add(x: float, y: float) -> float:
    """Returns the sum of x and y."""
    return x + y

def subtract(x: float, y: float) -> float:
    """Returns the difference of x and y."""
    return x - y

def multiply(x: float, y: float) -> float:
    """Returns the product of x and y."""
    return x * y

def divide(x: float, y: float) -> float | str: # Can return float or error string
    """Returns the division of x by y, handles division by zero."""
    if y == 0:
        return "Error: Cannot divide by zero."
    return x / y

# Main part of the program (the user interface)
def run_calculator():
    print("Select operation:")
    print("1. Add")
    print("2. Subtract")
    print("3. Multiply")
    print("4. Divide")

    while True: # Loop for valid choice input
        choice = input("Enter choice (1/2/3/4): ").strip()
        if choice in ['1', '2', '3', '4']:
            break
        else:
            print("Invalid choice. Please enter 1, 2, 3, or 4.")

    try:
        num1 = float(input("Enter first number: "))
        num2 = float(input("Enter second number: "))
    except ValueError:
        print("Invalid input. Please enter numbers.")
        return # Exit if input is bad

    # Call the appropriate function based on choice
    result: float | str # Declare type hint for result

    if choice == '1':
        result = add(num1, num2)
        op_symbol = "+"
    elif choice == '2':
        result = subtract(num1, num2)
        op_symbol = "-"
    elif choice == '3':
        result = multiply(num1, num2)
        op_symbol = "*"
    elif choice == '4':
        result = divide(num1, num2)
        op_symbol = "/"

    # Display the result (check if it's an error string)
    if isinstance(result, str): # Check if the result is the error message
        print(result)
    else:
        print(f"{num1} {op_symbol} {num2} = {result:.2f}") # Format float result


# Run the calculator if this script is executed directly
if __name__ == "__main__":
    run_calculator()

This structure clearly separates the calculation logic (in the functions) from the user interaction (in run_calculator). It’s much easier to test, modify, and understand.


7. Chapter Summary

  • Functions (def) let you package code into named, reusable blocks, promoting the DRY principle.
  • Use docstrings to explain what your functions do.
  • Pass data in via parameters (in definition) and arguments (in call). Use keyword arguments for clarity. Provide default values for optional parameters.
  • Use return to send a value back from a function; otherwise, it returns None.
  • Variables inside functions have local scope. Avoid over-reliance on global variables.
  • Use type hints (param: type -> return_type) for better code documentation and tooling support.

Functions help organize actions, but how do you organize collections of related data? Let’s explore Python’s powerful built-in data structures: lists, tuples, and dictionaries.