← Back to Tutorials Chapter 3

Functions & Modules

Defining Functions

Functions are reusable blocks of code defined with def:

def greet(name):
    """Return a greeting string."""
    return f"Hello, {name}!"

print(greet("Alice"))  # Hello, Alice!

Parameters

Python offers flexible parameter options:

Positional & Keyword Arguments

def describe_pet(animal, name):
    print(f"{name} is a {animal}")

describe_pet("dog", "Rover")           # positional
describe_pet(name="Fluffy", animal="cat")  # keyword

Default Parameters

def power(base, exp=2):
    return base ** exp

print(power(3))     # 9 (3^2)
print(power(3, 4))  # 81 (3^4)
Important: Default parameter values are evaluated once at function definition time, not each call. Never use mutable defaults like [] or {} unless you understand the implications.

*args (Variable Positional Arguments)

def sum_all(*numbers):
    return sum(numbers)

print(sum_all(1, 2, 3, 4))  # 10

**kwargs (Variable Keyword Arguments)

def print_info(**info):
    for key, value in info.items():
        print(f"{key}: {value}")

print_info(name="Bob", age=25, city="NYC")

Return Values

Functions can return multiple values as a tuple:

def min_max(items):
    return min(items), max(items)

low, high = min_max([3, 1, 7, 2, 9])
print(low, high)  # 1 9

Scope (LEGB Rule)

Python resolves variable names in this order: LocalEnclosingGlobalBuilt-in.

x = "global"

def outer():
    x = "enclosing"
    def inner():
        x = "local"
        print(x)
    inner()

outer()  # local

Lambda Functions

Anonymous one-line functions created with lambda:

square = lambda x: x ** 2
print(square(5))  # 25

pairs = [(1, 'one'), (3, 'three'), (2, 'two')]
pairs.sort(key=lambda pair: pair[0])
print(pairs)  # [(1, 'one'), (2, 'two'), (3, 'three')]

Function Annotations

def add(a: int, b: int) -> int:
    return a + b

Annotations are hints — they are not enforced at runtime but are useful for type checkers (e.g. mypy).

Modules and Imports

# Import a whole module
import math
print(math.sqrt(16))

# Import specific names
from random import randint, choice

# Import with alias
import datetime as dt

# Import all (discouraged)
from os import *

The __name__ Variable

# mymodule.py
def main():
    print("Running directly")

if __name__ == "__main__":
    main()

Packages

A package is a directory containing an __init__.py file (can be empty):

mypackage/
    __init__.py
    utils.py
    models.py

Useful Built-in Functions

len("hello")       # 5
range(10)          # 0..9
enumerate(["a","b"])  # (0,'a'), (1,'b')
zip([1,2], ["a","b"]) # (1,'a'), (2,'b')
sorted([3,1,2])    # [1,2,3]
map(str.upper, ["a","b"])  # ['A','B']
filter(lambda x: x>0, [-1,2,-3,4])  # [2,4]
Python function and module structure
Note: Use if __name__ == "__main__" to make a Python file both importable and runnable as a script.

Practice Exercise

Create a module math_utils.py with functions add, subtract, multiply, divide, and power. Add default parameter values where appropriate. Write a main.py that imports the module and demonstrates each function. Include a if __name__ guard in both files.