Defining Functions
Function Basics
# Simple function
def greet(name):
return f'Hello, {name}!'
# Function with multiple parameters
def calculate_area(length, width):
area = length * width
return area
# Function with no return value (returns None)
def print_hello():
print('Hello!')
result = print_hello()
print(result) # None
Docstrings
def calculate_average(numbers):
"""Calculate the average of a list of numbers.
Args:
numbers: A list of numeric values.
Returns:
The average of the numbers.
Raises:
ValueError: If the list is empty.
"""
if not numbers:
raise ValueError('Cannot calculate average of empty list')
return sum(numbers) / len(numbers)
# Access docstring
calculate_average.__doc__
help(calculate_average)
Multiple Return Values
# Functions can return multiple values
def get_min_max(numbers):
return min(numbers), max(numbers)
minimum, maximum = get_min_max([3, 1, 4, 1, 5])
print(f'Min: {minimum}, Max: {maximum}') # Min: 1, Max: 5
# Return as tuple
result = get_min_max([3, 1, 4, 1, 5])
print(result) # (1, 5)
print(type(result)) # <class 'tuple'>
Parameters & Arguments
Default Arguments
def greet(name, greeting='Hello'):
return f'{greeting}, {name}!'
greet('Alice') # 'Hello, Alice!'
greet('Alice', 'Hi') # 'Hi, Alice!'
⚠️ Mutable Default Arguments
# WRONG: Mutable default argument
items = [] # Shared across calls!
def add_item(item, items=[]):
items.append(item)
return items
add_item(1) # [1]
add_item(2) # [1, 2] - unexpected!
# RIGHT: Use None as default
items = []
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
add_item(1) # [1]
add_item(2) # [2] - correct!
Keyword Arguments
def create_user(name, age, email):
return {'name': name, 'age': age, 'email': email}
# Positional arguments
create_user('Alice', 30, 'alice@example.com')
# Keyword arguments (order doesn't matter)
create_user(email='alice@example.com', name='Alice', age=30)
# Mix (positional must come first)
create_user('Alice', email='alice@example.com', age=30)
*args and **kwargs
# *args - variable positional arguments
def calculate_sum(*args):
return sum(args)
calculate_sum(1, 2, 3) # 6
calculate_sum(1, 2, 3, 4, 5) # 15
# **kwargs - variable keyword arguments
def print_info(**kwargs):
for key, value in kwargs.items():
print(f'{key}: {value}')
print_info(name='Alice', age=30)
# name: Alice
# age: 30
# Combined
def func(required, *args, **kwargs):
print(f'Required: {required}')
print(f'Args: {args}')
print(f'Kwargs: {kwargs}')
func('hello', 1, 2, 3, key1='value1', key2='value2')
# Required: hello
# Args: (1, 2, 3)
# Kwargs: {'key1': 'value1', 'key2': 'value2'}
Unpacking Arguments
def add(a, b, c):
return a + b + c
numbers = [1, 2, 3]
add(*numbers) # 6 (unpacks list)
kwargs = {'a': 1, 'b': 2, 'c': 3}
add(**kwargs) # 6 (unpacks dictionary)
Variable Scope
LEGB Rule
Python resolves variables using the LEGB rule:
- Local - inside the function
- Enclosing - in enclosing function (nested)
- Global - at module level
- Built-in - Python's built-in names
# Global scope
global_var = 'I am global'
def outer():
# Enclosing scope
outer_var = 'I am enclosing'
def inner():
# Local scope
inner_var = 'I am local'
print(global_var) # ✅ Access global
print(outer_var) # ✅ Access enclosing
print(inner_var) # ✅ Access local
inner()
outer()
global Keyword
count = 0
def increment():
global count # Declare use of global variable
count += 1
increment()
print(count) # 1
# ⚠️ Avoid when possible - use return values instead
def increment_better(current_count):
return current_count + 1
nonlocal Keyword
def counter():
count = 0
def increment():
nonlocal count # Access enclosing variable
count += 1
return count
return increment
c = counter()
print(c()) # 1
print(c()) # 2
print(c()) # 3
Closures
def make_multiplier(factor):
def multiplier(x):
return x * factor # 'factor' is captured
return multiplier
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
Common Scope Pitfalls
# ⚠️ UnboundLocalError
x = 10
def func():
print(x) # UnboundLocalError!
x = 20 # Python thinks x is local
# Fix: Use global or pass as argument
def func():
global x
print(x) # 10
First-Class Functions
# Functions are objects - can be:
# 1. Assigned to variables
def add(a, b):
return a + b
my_func = add
print(my_func(2, 3)) # 5
# 2. Passed as arguments
def apply(func, a, b):
return func(a, b)
print(apply(add, 2, 3)) # 5
# 3. Returned from functions
def get_operation(op):
if op == 'add':
return lambda a, b: a + b
elif op == 'mul':
return lambda a, b: a * b
add_op = get_operation('add')
print(add_op(2, 3)) # 5
# 4. Stored in data structures
operations = {
'add': lambda a, b: a + b,
'sub': lambda a, b: a - b,
}
print(operations['add'](5, 3)) # 8