Functions
Comprehensive guide on Functions.
Functions
Overview#
Functions are reusable blocks of code that perform a specific task, letting you avoid repetition and organize a program into logical, testable pieces. This notebook covers defining and calling functions, the many ways Python lets you pass arguments (positional, keyword, default, variadic), return values, variable scope, lambda expressions, recursion, and the concept of closures. A solid grasp of functions is essential before moving on to decorators, generators, and object-oriented code, all of which build directly on these ideas.
1. Defining and Calling Functions#
Python 3.12 (WASM)def greet(name):
return f"Hello, {name}!"
message = greet("Alice")
print(message) # Hello, Alice!
A function that doesn't explicitly return anything returns None by
default.
Python 3.12 (WASM)def log_message(msg):
print(f"[LOG] {msg}")
result = log_message("Starting up") # prints, but...
print(result) # None — no explicit return
2. Parameters and Arguments#
Positional arguments#
Python 3.12 (WASM)def describe_pet(animal, name):
return f"{name} is a {animal}"
print(describe_pet("dog", "Rex")) # matched by position: animal="dog", name="Rex"
Keyword arguments#
Python 3.12 (WASM)print(describe_pet(name="Rex", animal="dog")) # order doesn't matter with keywords
print(describe_pet("dog", name="Rex")) # mixing positional + keyword is fine
Default argument values#
Python 3.12 (WASM)def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
print(greet("Alice")) # Hello, Alice!
print(greet("Bob", "Hi")) # Hi, Bob!
print(greet("Cara", greeting="Hey")) # Hey, Cara!
Parameters with defaults must come after parameters without defaults in the function definition.
Python 3.12 (WASM)# def broken(greeting="Hello", name): # SyntaxError: non-default argument follows default
# ...
The mutable default argument pitfall#
Python 3.12 (WASM)# Dangerous: the same list object is reused across every call
def add_item(item, items=[]):
items.append(item)
return items
print(add_item("apple")) # ['apple']
print(add_item("banana")) # ['apple', 'banana'] <- unexpected!
# Correct pattern
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
Default argument values are evaluated once, when the function is
defined, not each time it's called — mutable defaults like [] or {}
persist and accumulate state across calls unless you guard against it.
3. Variadic Arguments: *args and **kwargs#
Sometimes a function needs to accept an arbitrary number of arguments.
Python 3.12 (WASM)def total(*numbers): # *args collects extra positional args into a tuple
return sum(numbers)
print(total(1, 2, 3)) # 6
print(total(1, 2, 3, 4, 5)) # 15
print(total()) # 0
def build_profile(**details): # **kwargs collects extra keyword args into a dict
return details
print(build_profile(name="Alice", age=30, city="Bengaluru"))
# {'name': 'Alice', 'age': 30, 'city': 'Bengaluru'}
Combining all argument types#
Python 3.12 (WASM)def process(required, *args, default_value=10, **kwargs):
print("required:", required)
print("args:", args)
print("default_value:", default_value)
print("kwargs:", kwargs)
process(1, 2, 3, default_value=99, extra="data")
# required: 1
# args: (2, 3)
# default_value: 99
# kwargs: {'extra': 'data'}
The order in a function definition must be: standard positional
parameters, then *args, then keyword-only parameters (with or without
defaults), then **kwargs.
Unpacking arguments when calling a function#
Python 3.12 (WASM)def add(a, b, c):
return a + b + c
numbers = [1, 2, 3]
print(add(*numbers)) # unpacks the list as positional arguments: add(1, 2, 3)
values = {"a": 1, "b": 2, "c": 3}
print(add(**values)) # unpacks the dict as keyword arguments
4. Keyword-Only and Positional-Only Parameters#
Python lets you explicitly force certain parameters to be passed only by keyword, or only by position, using special markers in the function signature.
Python 3.12 (WASM)def create_user(name, *, email, role="user"):
# everything after * must be passed as a keyword argument
return f"{name} ({email}), role={role}"
create_user("Alice", email="a@example.com") # OK
# create_user("Alice", "a@example.com") # TypeError: too many positional args
def divide(a, b, /):
# everything before / must be passed positionally, not by keyword
return a / b
divide(10, 2) # OK
# divide(a=10, b=2) # TypeError: positional-only arguments
This is mostly useful for API design — keyword-only arguments make call sites self-documenting for less obvious parameters, while positional-only arguments protect an implementation detail (a parameter name) from becoming part of the function's public contract.
5. Return Values#
Python 3.12 (WASM)def square(n):
return n ** 2
# Returning multiple values (actually returns a single tuple)
def min_max(numbers):
return min(numbers), max(numbers)
low, high = min_max([4, 1, 9, 3])
print(low, high) # 1 9
# Early return for guard clauses
def safe_divide(a, b):
if b == 0:
return None
return a / b
# A function can have multiple return points
def classify(n):
if n < 0:
return "negative"
if n == 0:
return "zero"
return "positive"
6. Variable Scope#
Python resolves names using the LEGB rule: Local, Enclosing, Global, Built-in — searched in that order.
Python 3.12 (WASM)x = "global" # global scope
def outer():
x = "enclosing" # enclosing scope (relative to inner())
def inner():
x = "local" # local scope
print(x) # local
inner()
print(x) # enclosing
outer()
print(x) # global
Reading vs modifying an outer variable#
Python 3.12 (WASM)counter = 0
def increment():
print(counter) # OK — reading a global variable works without declaring anything
def broken_increment():
counter += 1 # UnboundLocalError! Assigning makes Python treat it as local
def fixed_increment():
global counter # explicitly declare intent to modify the global variable
counter += 1
fixed_increment()
print(counter) # 1
Assigning to a variable inside a function makes Python treat it as local
for the entire function body, even before the assignment line — this
is a common source of confusing UnboundLocalErrors for beginners. Use
global (or nonlocal for enclosing-scope variables in nested functions)
to explicitly opt into modifying an outer variable.
Python 3.12 (WASM)def make_counter():
count = 0
def increment():
nonlocal count # refers to the enclosing function's 'count'
count += 1
return count
return increment
counter = make_counter()
print(counter()) # 1
print(counter()) # 2
7. Lambda Expressions#
A lambda is a small, anonymous, single-expression function — useful for
short throwaway functions, especially as arguments to other functions.
Python 3.12 (WASM)square = lambda x: x ** 2
print(square(5)) # 25
add = lambda a, b: a + b
print(add(3, 4)) # 7
# Common use: as a key function for sorting
people = [("Alice", 30), ("Bob", 25), ("Cara", 35)]
people.sort(key=lambda p: p[1])
print(people) # [('Bob', 25), ('Alice', 30), ('Cara', 35)]
# Common use: with map() and filter()
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda n: n ** 2, numbers))
evens = list(filter(lambda n: n % 2 == 0, numbers))
A lambda can only contain a single expression, not statements like
if/for blocks or multiple lines. For anything more complex than a
one-liner, a regular def function is clearer and should be preferred —
overusing lambdas hurts readability.
8. Recursion#
A recursive function calls itself to solve a smaller version of the same problem, until reaching a base case that stops the recursion.
Python 3.12 (WASM)def factorial(n):
if n <= 1: # base case — stops the recursion
return 1
return n * factorial(n - 1) # recursive case
print(factorial(5)) # 120
Python 3.12 (WASM)def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(10)) # 55
Recursion limits and stack depth#
Python 3.12 (WASM)import sys
print(sys.getrecursionlimit()) # 1000 by default
def infinite_recursion(n):
return infinite_recursion(n + 1)
# infinite_recursion(0) # eventually raises RecursionError
Every recursive call adds a new frame to the call stack; excessively deep
recursion (or a missing/incorrect base case) raises RecursionError. For
problems with very deep recursion, an iterative solution (using a loop) is
often more practical in Python, since Python doesn't optimize "tail calls"
the way some other languages do.
9. Docstrings and Function Documentation#
Python 3.12 (WASM)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: The calculated BMI value.
"""
return weight_kg / (height_m ** 2)
print(calculate_bmi.__doc__) # prints the docstring
help(calculate_bmi) # shows a formatted help page using the docstring
10. Higher-Order Functions#
Since functions are first-class objects in Python (covered in more depth in the Decorators notebook), they can be passed as arguments to other functions and returned from them.
Python 3.12 (WASM)def apply_operation(func, a, b):
return func(a, b)
def add(a, b):
return a + b
def multiply(a, b):
return a * b
print(apply_operation(add, 3, 4)) # 7
print(apply_operation(multiply, 3, 4)) # 12
Notes#
- A function without an explicit
returnreturnsNone, not an error and not0. - Default argument values are evaluated once, at function definition time
— avoid mutable defaults like
[]or{}. *argscollects extra positional arguments as a tuple;**kwargscollects extra keyword arguments as a dict — the namesargs/kwargsare convention, not required syntax (the*/**do the actual work).- Assigning to a name anywhere inside a function makes Python treat that
name as local for the whole function body — use
global/nonlocalexplicitly when you intend to modify an outer-scope variable. - Lambdas are limited to a single expression; use a regular
deffor anything requiring multiple statements or complex logic. - Recursive functions must have a clearly reachable base case, or they
will eventually raise
RecursionError.
Best Practices Checklist#
- Give every function a clear, descriptive name and, ideally, a docstring
- Avoid mutable default argument values (
def f(x=[])) - Use keyword arguments for optional/less-obvious parameters at call sites, to improve readability
- Prefer explicit parameters over
**kwargswhen the expected arguments are known in advance - Use
global/nonlocalsparingly — prefer passing values in and returning values out over mutating outer-scope state - Keep lambdas to simple, single-expression use cases; use
deffor anything more complex - Always ensure recursive functions have a correct, reachable base case
- Prefer an iterative solution over deep recursion when working with very large inputs, due to Python's recursion depth limit