Free GuideBeginner
40 min read
#python#functions#arguments#scope#lambda#recursion#closures

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 return returns None, not an error and not 0.
  • Default argument values are evaluated once, at function definition time — avoid mutable defaults like [] or {}.
  • *args collects extra positional arguments as a tuple; **kwargs collects extra keyword arguments as a dict — the names args/kwargs are 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/nonlocal explicitly when you intend to modify an outer-scope variable.
  • Lambdas are limited to a single expression; use a regular def for 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 **kwargs when the expected arguments are known in advance
  • Use global/nonlocal sparingly — prefer passing values in and returning values out over mutating outer-scope state
  • Keep lambdas to simple, single-expression use cases; use def for 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
Knowledge Checkpoint

Functions, Closures & LEGB Scope Checkpoint

Q1.In Python's LEGB scope lookup order, what does the 'E' stand for?
AExternal
BEnclosing
CEnvironment
DExported
Q2.Which keyword must you use inside an inner nested function to reassign a variable defined in the outer function's scope?
Aglobal
Bnonlocal
Couter
Dparent
Q3.What is captured inside a Python function closure when an inner function accesses an outer variable?
AA static frozen copy of the value at the moment of function creation.
BA cell object reference that dynamically reflects updates to the variable in the outer scope.
CA deep copy of the global environment.
DThe compiled bytecode of the outer function.
Q4.What do `*args` and `**kwargs` pack positional and keyword parameters into, respectively?
A`*args` into a list, `**kwargs` into a dictionary
B`*args` into a tuple, `**kwargs` into a dictionary
C`*args` into a set, `**kwargs` into a tuple
D`*args` into a generator, `**kwargs` into a JSON string
Track Your Learning

Finished studying this notebook?

Mark this guide as completed to update your course progress roadmap.