Defining Functions
Functions are reusable blocks of code that perform a specific task. Instead of writing the same logic over and over, you define it once as a function and call it whenever you need it. This makes your code shorter, more organized, and easier to debug — if something's wrong, you fix it in one place.
In Python, you define a function with the def keyword, followed by a name, parentheses with optional parameters, and a colon. The function body is indented:
def greet(name):
"""Greet a person by name."""
return f"Hello, {name}!"
# Call the function
message = greet("Alice")
print(message)
# Functions without return give None
def say_hello(name):
print(f"Hi, {name}!")
result = say_hello("Bob")
print(f"Return value: {result}")
Hello, Alice! Hi, Bob! Return value: None
The triple-quoted string right after def is called a docstring. It describes what the function does and is accessible via help(greet) or greet.__doc__. Always write docstrings for public functions.
Functions are first-class objects
In Python, functions are objects just like integers or strings. You can assign them to variables, pass them as arguments, return them from other functions, and store them in data structures. This is the foundation of functional programming in Python.
Default Arguments
Give parameters default values so callers can omit them. Parameters with defaults must come after parameters without:
def power(base, exp=2):
"""Raise base to exp. Defaults to squaring."""
return base ** exp
print(power(3)) # 9 (uses default exp=2)
print(power(3, 3)) # 27
print(power(2, 10)) # 1024
9 27 1024
*args and **kwargs
Sometimes you don't know in advance how many arguments a function will receive. Python provides two special syntaxes: *args collects extra positional arguments into a tuple, and **kwargs collects extra keyword arguments into a dictionary.
Think of *args as "any number of values" and **kwargs as "any number of named values":
def describe(*args, **kwargs):
print(f"Positional args: {args}")
print(f"Keyword args: {kwargs}")
describe(1, 2, 3, name="Alice", age=30)
Positional args: (1, 2, 3)
Keyword args: {'name': 'Alice', 'age': 30}Practical Example: Flexible Logger
def log(message, *values, sep=", ", level="INFO"):
"""Log a message with optional values."""
formatted = sep.join(str(v) for v in values)
if formatted:
print(f"[{level}] {message}: {formatted}")
else:
print(f"[{level}] {message}")
log("Starting app")
log("User scores", 95, 87, 92)
log("Error occurred", "file not found", level="ERROR")
[INFO] Starting app [INFO] User scores: 95, 87, 92 [ERROR] Error occurred: file not found
*args, then keyword-only params, then **kwargs. Example: def f(a, b, *args, option=True, **kwargs)Lambda Functions
Lambda functions are small, anonymous functions defined in a single line. They're useful when you need a quick function for a short-lived purpose — like a sort key or a filter condition. Think of them as throwaway functions:
# Regular function
def square(x):
return x ** 2
# Same thing as a lambda
square = lambda x: x ** 2
print(square(5)) # 25
# Lambdas shine as arguments to other functions
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
# Sort by absolute distance from 5
sorted_nums = sorted(numbers, key=lambda x: abs(x - 5))
print(sorted_nums)
# Filter: keep only even numbers
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens)
# Map: double each number
doubled = list(map(lambda x: x * 2, numbers))
print(doubled)
25 [5, 4, 6, 3, 2, 9, 1, 1] [4, 2, 6] [6, 2, 8, 2, 10, 18, 4, 12]
When to Use Lambda vs def
Use lambda for simple, one-expression functions passed as arguments (sort keys, map/filter). Use def for anything more than one expression, anything you'll reuse, or anything that needs a docstring. If a lambda is hard to read, refactor it to a named function.
Scope: The LEGB Rule
When Python sees a variable name, it searches for it in four places, in this order: Local (inside the current function), Enclosing (inside any enclosing functions), Global (module level), Built-in (Python's built-in names like print and len). This is called the LEGB rule.
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(f"inner: {x}")
inner()
print(f"outer: {x}")
outer()
print(f"module: {x}")
inner: local outer: enclosing module: global
Each function has its own local scope. Assigning to a variable inside a function creates a new local variable — it doesn't modify the outer one. If you need to modify an outer variable, use nonlocal (for enclosing scope) or global (for module scope), though these should be used sparingly.
Higher-Order Functions
A higher-order function is one that takes another function as an argument or returns a function. This is possible because Python functions are first-class objects. You've already seen this with sorted(key=...) and map():
def apply_twice(func, value):
"""Apply a function twice to a value."""
return func(func(value))
def add_three(x):
return x + 3
result = apply_twice(add_three, 7)
print(result) # add_three(add_three(7)) = add_three(10) = 13
# Returning a function (closure)
def make_multiplier(factor):
def multiply(x):
return x * factor
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
13 10 15
🔍 Deep Dive: Closures
When make_multiplier(2) returns the inner multiply function, that function "remembers" that factor is 2 — even after make_multiplier has finished executing. This is called a closure: the inner function closes over (captures) variables from its enclosing scope. Closures are the foundation of decorators and many design patterns in Python.
⚠️ Common Mistake: Mutable Default Arguments
Wrong:
def add_item(item, lst=[]):
lst.append(item)
return lst
print(add_item("a")) # ['a']
print(add_item("b")) # ['a', 'b'] — Bug! List persists!
Why: Default mutable arguments (lists, dicts) are created once when the function is defined and shared across all calls.
Instead:
def add_item(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst
print(add_item("a")) # ['a']
print(add_item("b")) # ['b'] — correct!
Quick Reference
| Concept | Syntax | Example |
|---|---|---|
| Define function | def name(params): | def greet(name): |
| Default value | param=value | def f(x, y=10): |
| Variable positional | *args | def f(*args): |
| Variable keyword | **kwargs | def f(**kwargs): |
| Lambda | lambda params: expr | lambda x: x * 2 |
| Docstring | """text""" | First line after def |
| Return | return value | None if omitted |