1.4. Functions & Modules

Overview

Organize your code into reusable chunks, make it more readable, and start building real programs by leveraging Python's modular design.

Functions are one of the most powerful tools in programming. They allow you to break down complex problems into smaller, manageable pieces that you can reuse throughout your code. Think of functions as mini-programs within your program—each with a specific job to do. Combined with modules (collections of functions and other code), they form the foundation of well-organized, maintainable Python programs.

In this chapter, you'll learn how to write your own functions, understand how Python manages variables and memory, explore Python's vast standard library, and create your own modules. By the end, you'll be writing cleaner, more efficient code that follows professional programming practices.

Defining Functions

What is a Function?

A function is a named block of code that performs a specific task. Instead of writing the same code multiple times, you can define it once as a function and then "call" (use) it whenever needed. This makes your code:

Basic Function Syntax

Here's the basic syntax for defining a function in Python:

def function_name(parameters):
    """Optional docstring describing what the function does"""
    # Function body - the code that does the work
    return result  # Optional return statement

Let's start with a simple example:

def greet():
    """Prints a friendly greeting"""
    print("Hello, welcome to Python functions!")

# Call the function
greet()  # Output: Hello, welcome to Python functions!

Functions with Parameters

Most functions need input to work with. Parameters (also called arguments) allow you to pass data into your function:

def greet_person(name):
    """Greets a specific person by name"""
    print(f"Hello, {name}! Welcome to Python functions!")

# Call the function with an argument
greet_person("Alice")  # Output: Hello, Alice! Welcome to Python functions!
greet_person("Bob")    # Output: Hello, Bob! Welcome to Python functions!

You can have multiple parameters:

def calculate_area(length, width):
    """Calculates the area of a rectangle"""
    area = length * width
    print(f"The area of a {length}x{width} rectangle is {area}")

calculate_area(5, 3)   # Output: The area of a 5x3 rectangle is 15
calculate_area(10, 7)  # Output: The area of a 10x7 rectangle is 70

Return Values

Functions can send data back to the code that called them using the return statement:

def add_numbers(a, b):
    """Adds two numbers and returns the result"""
    result = a + b
    return result

# Store the returned value in a variable
sum_result = add_numbers(5, 3)
print(sum_result)  # Output: 8

# Or use the returned value directly
print(add_numbers(10, 20))  # Output: 30

You can return multiple values as a tuple:

def calculate_rectangle(length, width):
    """Calculates both area and perimeter of a rectangle"""
    area = length * width
    perimeter = 2 * (length + width)
    return area, perimeter

# Unpack the returned values
rect_area, rect_perimeter = calculate_rectangle(5, 3)
print(f"Area: {rect_area}, Perimeter: {rect_perimeter}")
# Output: Area: 15, Perimeter: 16

Default Parameters

You can provide default values for parameters, making them optional:

def greet_with_title(name, title="Friend"):
    """Greets someone with an optional title"""
    print(f"Hello, {title} {name}!")

greet_with_title("Alice")           # Output: Hello, Friend Alice!
greet_with_title("Bob", "Dr.")      # Output: Hello, Dr. Bob!
greet_with_title("Carol", "Prof.")  # Output: Hello, Prof. Carol!

Practical Example: Temperature Converter

Let's create a more practical function that converts temperatures:

def celsius_to_fahrenheit(celsius):
    """Converts Celsius to Fahrenheit"""
    fahrenheit = (celsius * 9/5) + 32
    return fahrenheit

def fahrenheit_to_celsius(fahrenheit):
    """Converts Fahrenheit to Celsius"""
    celsius = (fahrenheit - 32) * 5/9
    return celsius

def temperature_converter(temp, from_scale, to_scale):
    """Universal temperature converter"""
    if from_scale.lower() == "celsius" and to_scale.lower() == "fahrenheit":
        return celsius_to_fahrenheit(temp)
    elif from_scale.lower() == "fahrenheit" and to_scale.lower() == "celsius":
        return fahrenheit_to_celsius(temp)
    else:
        return f"Conversion from {from_scale} to {to_scale} not supported"

# Test the functions
print(f"25°C = {celsius_to_fahrenheit(25):.1f}°F")  # Output: 25°C = 77.0°F
print(f"77°F = {fahrenheit_to_celsius(77):.1f}°C")  # Output: 77°F = 25.0°C
print(temperature_converter(100, "Celsius", "Fahrenheit"))  # Output: 212.0

Scope & Lifetime

Understanding Variable Scope

Scope determines where in your code a variable can be accessed. Python has several levels of scope:

  1. Local Scope: Variables defined inside a function
  2. Global Scope: Variables defined at the module level
  3. Built-in Scope: Pre-defined names in Python
# Global variable
global_var = "I'm global!"

def my_function():
    # Local variable
    local_var = "I'm local!"
    print(global_var)  # Can access global variable
    print(local_var)   # Can access local variable

my_function()
print(global_var)  # Can access global variable
# print(local_var)  # This would cause an error - local_var doesn't exist here

The LEGB Rule

Python follows the LEGB rule when looking for variables:

x = "global x"

def outer_function():
    x = "enclosing x"

    def inner_function():
        x = "local x"
        print(f"Inner function sees: {x}")

    inner_function()
    print(f"Outer function sees: {x}")

outer_function()
print(f"Global scope sees: {x}")

# Output:
# Inner function sees: local x
# Outer function sees: enclosing x
# Global scope sees: global x

Modifying Global Variables

To modify a global variable inside a function, use the global keyword:

counter = 0  # Global variable

def increment_counter():
    global counter
    counter += 1
    print(f"Counter is now: {counter}")

increment_counter()  # Output: Counter is now: 1
increment_counter()  # Output: Counter is now: 2
print(f"Final counter value: {counter}")  # Output: Final counter value: 2

Variable Lifetime

Variables exist only as long as their scope is active:

def demonstrate_lifetime():
    temp_var = "I exist only during function execution"
    print(temp_var)
    return temp_var

result = demonstrate_lifetime()  # temp_var is created and destroyed
print(result)  # But its value is preserved in 'result'

Importing Modules

What are Modules?

A module is a file containing Python code. It can define functions, classes, and variables, and can also include runnable code. Python comes with a huge standard library of modules that provide pre-written functionality for common tasks.

Basic Import Syntax

There are several ways to import modules:

# Import the entire module
import math
print(math.pi)        # Output: 3.141592653589793
print(math.sqrt(16))  # Output: 4.0

# Import specific functions
from math import pi, sqrt
print(pi)        # Output: 3.141592653589793
print(sqrt(25))  # Output: 5.0

# Import with an alias
import math as m
print(m.factorial(5))  # Output: 120

# Import all (generally not recommended)
from math import *
print(cos(0))  # Output: 1.0

Useful Standard Library Modules

Let's explore some commonly used modules:

The random Module

import random

# Generate random numbers
print(random.randint(1, 10))        # Random integer between 1 and 10
print(random.random())              # Random float between 0 and 1
print(random.uniform(1.5, 10.5))   # Random float between 1.5 and 10.5

# Work with sequences
colors = ["red", "green", "blue", "yellow"]
print(random.choice(colors))        # Pick a random color
random.shuffle(colors)              # Shuffle the list in place
print(colors)                       # List is now in random order

The datetime Module

from datetime import datetime, date, timedelta

# Current date and time
now = datetime.now()
print(f"Current date and time: {now}")

# Just the date
today = date.today()
print(f"Today's date: {today}")

# Date arithmetic
tomorrow = today + timedelta(days=1)
next_week = today + timedelta(weeks=1)
print(f"Tomorrow: {tomorrow}")
print(f"Next week: {next_week}")

The os Module

import os

# Get current working directory
current_dir = os.getcwd()
print(f"Current directory: {current_dir}")

# List files in a directory
files = os.listdir('.')
print(f"Files in current directory: {files}")

# Check if a file exists
if os.path.exists('my_file.txt'):
    print("File exists!")
else:
    print("File not found.")

Built-in Functions

Python provides many built-in functions that are always available without importing. Let's explore some of the most useful ones:

Mathematical Functions

numbers = [1, 2, 3, 4, 5]

print(len(numbers))    # Output: 5 (length of the list)
print(sum(numbers))    # Output: 15 (sum of all numbers)
print(min(numbers))    # Output: 1 (smallest number)
print(max(numbers))    # Output: 5 (largest number)

# Absolute value
print(abs(-10))        # Output: 10

# Round numbers
print(round(3.14159, 2))  # Output: 3.14 (round to 2 decimal places)

Type Conversion Functions

# Convert between types
print(int("42"))       # Output: 42 (string to integer)
print(float("3.14"))   # Output: 3.14 (string to float)
print(str(123))        # Output: "123" (integer to string)
print(bool(1))         # Output: True (integer to boolean)
print(bool(0))         # Output: False

# Create collections
print(list("hello"))   # Output: ['h', 'e', 'l', 'l', 'o']
print(tuple([1, 2, 3])) # Output: (1, 2, 3)
print(set([1, 2, 2, 3])) # Output: {1, 2, 3} (duplicates removed)

The range() Function

# Create sequences of numbers
print(list(range(5)))        # Output: [0, 1, 2, 3, 4]
print(list(range(2, 8)))     # Output: [2, 3, 4, 5, 6, 7]
print(list(range(0, 10, 2))) # Output: [0, 2, 4, 6, 8] (step of 2)

# Use with loops
for i in range(3):
    print(f"Iteration {i}")

The enumerate() Function

fruits = ["apple", "banana", "cherry"]

# Get both index and value
for index, fruit in enumerate(fruits):
    print(f"{index}: {fruit}")

# Output:
# 0: apple
# 1: banana
# 2: cherry

The zip() Function

names = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 35]
cities = ["New York", "London", "Tokyo"]

# Combine multiple lists
for name, age, city in zip(names, ages, cities):
    print(f"{name} is {age} years old and lives in {city}")

# Output:
# Alice is 25 years old and lives in New York
# Bob is 30 years old and lives in London
# Charlie is 35 years old and lives in Tokyo

Your First Module

Creating a Module

A module is simply a Python file. Let's create a module called calculator.py:

# calculator.py
"""
A simple calculator module with basic mathematical operations.
"""

def add(a, b):
    """Returns the sum of two numbers"""
    return a + b

def subtract(a, b):
    """Returns the difference of two numbers"""
    return a - b

def multiply(a, b):
    """Returns the product of two numbers"""
    return a * b

def divide(a, b):
    """Returns the quotient of two numbers"""
    if b == 0:
        return "Error: Cannot divide by zero"
    return a / b

def power(base, exponent):
    """Returns base raised to the power of exponent"""
    return base ** exponent

# Module-level variables
PI = 3.14159
EULER_NUMBER = 2.71828

# This code runs when the module is imported
print("Calculator module loaded successfully!")

Using Your Module

Now create another file in the same directory to use your module:

# main.py
import calculator

# Use functions from the module
result1 = calculator.add(10, 5)
result2 = calculator.multiply(3, 4)
result3 = calculator.power(2, 8)

print(f"10 + 5 = {result1}")      # Output: 10 + 5 = 15
print(f"3 * 4 = {result2}")       # Output: 3 * 4 = 12
print(f"2^8 = {result3}")         # Output: 2^8 = 256

# Access module variables
print(f"Pi is approximately {calculator.PI}")

# Import specific functions
from calculator import divide, subtract

print(f"20 / 4 = {divide(20, 4)}")      # Output: 20 / 4 = 5.0
print(f"15 - 7 = {subtract(15, 7)}")    # Output: 15 - 7 = 8

Module Documentation and the __name__ Variable

You can make your module both importable and executable:

# calculator.py (enhanced version)
"""
A simple calculator module with basic mathematical operations.
"""

def add(a, b):
    """Returns the sum of two numbers"""
    return a + b

def subtract(a, b):
    """Returns the difference of two numbers"""
    return a - b

def multiply(a, b):
    """Returns the product of two numbers"""
    return a * b

def divide(a, b):
    """Returns the quotient of two numbers"""
    if b == 0:
        return "Error: Cannot divide by zero"
    return a / b

def main():
    """Demo function to show calculator capabilities"""
    print("Calculator Demo:")
    print(f"5 + 3 = {add(5, 3)}")
    print(f"10 - 4 = {subtract(10, 4)}")
    print(f"6 * 7 = {multiply(6, 7)}")
    print(f"15 / 3 = {divide(15, 3)}")

# This runs only when the file is executed directly, not when imported
if __name__ == "__main__":
    main()

Now you can both import the module and run it directly:

# Run the module directly
python calculator.py
# Output: Calculator Demo: [followed by the demo results]

# Or import it in another file without running the demo

Organizing Code with Packages

As your projects grow, you can organize related modules into packages (directories with an __init__.py file):

my_project/
    math_tools/
        __init__.py
        calculator.py
        geometry.py
    utils/
        __init__.py
        file_helpers.py
        string_helpers.py
    main.py

Best Practices and Tips

Function Design Principles

  1. Single Responsibility: Each function should do one thing well
  2. Descriptive Names: Use clear, descriptive function names
  3. Keep It Short: Functions should generally be no more than 20-30 lines
  4. Document Your Functions: Use docstrings to explain what functions do
def calculate_bmi(weight_kg, height_m):
    """
    Calculate Body Mass Index (BMI).

    Args:
        weight_kg (float): Weight in kilograms
        height_m (float): Height in meters

    Returns:
        float: BMI value
    """
    return weight_kg / (height_m ** 2)

Common Pitfalls to Avoid

  1. Mutable Default Arguments: Don't use mutable objects as default parameters
# Bad
def add_item(item, my_list=[]):
    my_list.append(item)
    return my_list

# Good
def add_item(item, my_list=None):
    if my_list is None:
        my_list = []
    my_list.append(item)
    return my_list
  1. Global Variable Overuse: Limit global variables and use parameters instead

Conclusion

Functions and modules are fundamental building blocks of Python programming. They help you:

Practice creating functions for common tasks in your programs, explore Python's standard library modules, and start organizing your code into modules as your projects grow. In the next section, we'll learn about error handling, which will help you make your functions and modules more robust and user-friendly.

The key to mastering functions and modules is practice. Start small, experiment with different approaches, and gradually build more complex functionality as you become comfortable with these concepts.