Python Functions

Functions are reusable blocks of code. Python functions are first-class objects — they can be passed as arguments, returned from other functions, and stored in variables.

Defining Functions

Python
# Basic function
def greet(name):
    """Greet a user by name."""  # Docstring (helps with documentation)
    return f"Hello, {name}!"

# Function with multiple parameters
def calculate_area(length, width):
    """Calculate the area of a rectangle."""
    return length * width

# Default parameters
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

greet("Alice")              # "Hello, Alice!"
greet("Bob", "Hi")          # "Hi, Bob!"

# Keyword arguments (can be in any order)
def create_user(name, age, email):
    return {"name": name, "age": age, "email": email}

user = create_user(age=28, email="alice@test.com", name="Alice")

*args and **kwargs

Python
# *args — variable number of positional arguments
def sum_all(*args):
    """Sum any number of arguments."""
    return sum(args)

print(sum_all(1, 2, 3))      # 6
print(sum_all(1, 2, 3, 4, 5))  # 15

# **kwargs — variable number of keyword arguments
def print_info(**kwargs):
    """Print any keyword arguments."""
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_info(name="Alice", age=28, city="NYC")

# Combining both
def func(required, *args, **kwargs):
    print(f"Required: {required}")
    print(f"Args: {args}")
    print(f"Kwargs: {kwargs}")

func("hello", 1, 2, 3, key="value")
# Required: hello
# Args: (1, 2, 3)
# Kwargs: {'key': 'value'}

Lambda Functions

Python
# Lambda — anonymous, one-line functions
square = lambda x: x ** 2
add = lambda a, b: a + b

print(square(5))   # 25
print(add(3, 4))   # 7

# Lambdas are most useful with higher-order functions
numbers = [5, 2, 8, 1, 9, 3]

# sorted with key function
sorted_numbers = sorted(numbers, key=lambda x: -x)  # [9, 8, 5, 3, 2, 1]

# With lists of tuples
students = [("Alice", 95), ("Bob", 87), ("Charlie", 92)]
by_grade = sorted(students, key=lambda s: s[1], reverse=True)
# [('Alice', 95), ('Charlie', 92), ('Bob', 87)]

# map — apply function to every element
doubled = list(map(lambda x: x * 2, [1, 2, 3]))  # [2, 4, 6]

# filter — keep elements that pass the test
evens = list(filter(lambda x: x % 2 == 0, range(10)))  # [0, 2, 4, 6, 8]

Higher-Order Functions

Python
# Functions that take or return other functions
def apply_twice(func, value):
    """Apply a function twice to a value."""
    return func(func(value))

print(apply_twice(lambda x: x + 3, 7))  # 13 (7+3=10, 10+3=13)

# Function composition
def compose(f, g):
    """Compose two functions: f(g(x))."""
    return lambda x: f(g(x))

double = lambda x: x * 2
increment = lambda x: x + 1

double_then_increment = compose(increment, double)
print(double_then_increment(5))  # 11 (5*2=10, 10+1=11)

Closures

Python
# A closure captures variables from its enclosing scope
def make_multiplier(factor):
    """Return a function that multiplies by factor."""
    def multiplier(x):
        return x * factor
    return multiplier

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(5))   # 10
print(triple(5))   # 15

# Practical: a simple counter
def make_counter(start=0):
    count = [start]  # List to allow mutation in closure
    def counter():
        count[0] += 1
        return count[0]
    return counter

c = make_counter()
print(c())  # 1
print(c())  # 2
print(c())  # 3

Decorators

Python
import time

# A decorator wraps a function with extra behavior
def timer(func):
    """Measure execution time."""
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} took {end-start:.4f}s")
        return result
    return wrapper

@timer  # Same as: slow_function = timer(slow_function)
def slow_function():
    time.sleep(1)
    return "Done!"

slow_function()  # "slow_function took 1.0012s"

# Practical: retry decorator
def retry(max_attempts=3):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts - 1:
                        raise
                    print(f"Attempt {attempt+1} failed: {e}")
        return wrapper
    return decorator

@retry(max_attempts=3)
def unreliable_function():
    import random
    if random.random() < 0.7:
        raise ValueError("Random failure!")
    return "Success!"

Knowledge Check

4 questions — test your understanding

1

What is the output of: list(map(lambda x: x*2, [1,2,3]))?

2

What does *args represent in a function definition?

3

What is a closure in Python?

4

What does @timer do above a function?