Python Functions — Core Concepts
Functions as First-Class Citizens
In Python, functions are objects — you can assign them to variables, pass them as arguments, and return them from other functions. This isn’t just a curiosity; it fundamentally changes how you can structure programs.
def greet(name):
return f"Hello, {name}!"
say_hello = greet # Assign function to a variable
print(say_hello("Alice")) # Hello, Alice!
Defining Functions
def function_name(parameters):
# body
return value # optional
If there’s no return statement, the function returns None.
Docstrings
By convention, functions start with a docstring — a string literal describing what the function does:
def calculate_bmi(weight_kg, height_m):
"""Calculate Body Mass Index from weight and height.
Args:
weight_kg: Weight in kilograms
height_m: Height in meters
Returns:
BMI as a float
"""
return weight_kg / (height_m ** 2)
Docstrings are accessible at runtime via help(calculate_bmi) and are used by documentation generators. Writing them isn’t optional in real projects.
Argument Flexibility
Default Arguments
Parameters can have default values, making them optional:
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
greet("Alice") # Hello, Alice!
greet("Bob", "Good morning") # Good morning, Bob!
Important: default values are evaluated once at function definition, not at call time. The mutable default argument bug (using [] or {} as defaults) comes from this. Use None as a sentinel instead.
Keyword Arguments
Callers can specify arguments by name, in any order:
def create_user(username, email, admin=False, active=True):
...
create_user(email="alice@example.com", username="alice")
create_user("bob", "bob@example.com", admin=True)
*args: Variable Positional Arguments
*args collects any number of positional arguments into a tuple:
def total(*numbers):
return sum(numbers)
total(1, 2, 3) # 6
total(10, 20, 30, 40) # 100
**kwargs: Variable Keyword Arguments
**kwargs collects keyword arguments into a dictionary:
def print_info(**details):
for key, value in details.items():
print(f"{key}: {value}")
print_info(name="Alice", age=30, city="London")
Combining Them All
You can combine all argument types, but order matters:
def mixed(pos1, pos2, *args, kw_only, **kwargs):
...
- Regular positional first
*argsnext- Keyword-only arguments (after
*args, must be passed by name) **kwargslast
Lambda Functions
lambda creates a small anonymous function in a single expression:
square = lambda x: x ** 2
square(5) # 25
# Most common use: as arguments to other functions
numbers = [3, 1, 4, 1, 5, 9]
sorted_nums = sorted(numbers, key=lambda x: -x) # Sort descending
Lambdas are limited to a single expression. For anything more complex, use a named function — it’s more readable and testable.
Higher-Order Functions
Functions that take or return other functions. Python has several built-in:
numbers = [1, 2, 3, 4, 5, 6]
# map: apply a function to each element
doubled = list(map(lambda x: x * 2, numbers))
# [2, 4, 6, 8, 10, 12]
# filter: keep only elements that pass a test
evens = list(filter(lambda x: x % 2 == 0, numbers))
# [2, 4, 6]
In modern Python, list comprehensions are often preferred over map/filter:
doubled = [x * 2 for x in numbers]
evens = [x for x in numbers if x % 2 == 0]
Same result, more readable.
Closures
A closure is a function that “captures” variables from its enclosing scope:
def make_multiplier(factor):
def multiply(x):
return x * factor # captures 'factor' from outer scope
return multiply
triple = make_multiplier(3)
print(triple(10)) # 30
print(triple(7)) # 21
multiply keeps a reference to factor even after make_multiplier has returned. This is useful for creating specialized functions without classes.
Decorators
Decorators are a clean syntax for wrapping one function with another:
def log_calls(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"Done: {func.__name__}")
return result
return wrapper
@log_calls
def add(a, b):
return a + b
add(2, 3)
# Calling add
# Done: add
@log_calls is syntactic sugar for add = log_calls(add). The original function is wrapped — every call to add now goes through wrapper first. This pattern powers Flask routes, Django views, pytest fixtures, and most Python frameworks.
Common Misconception: Functions Always Need return
Functions without explicit return aren’t broken — they return None. This is valid:
def print_separator():
print("-" * 40)
But if you write result = print_separator(), result is None. Watch for this when using functions in expressions.
One Thing to Remember
Python functions are objects — you can pass them around, store them in variables, and return them from other functions. This makes patterns like decorators, callbacks, and closures natural rather than awkward.
See Also
- Python Async Await Async/await helps one Python program juggle many waiting jobs at once, like a chef who keeps multiple pots moving without standing still.
- Python Basics Python is the programming language that reads like plain English — here's why millions of beginners (and experts) choose it first.
- Python Booleans Make Booleans click with one clear analogy you can reuse whenever Python feels confusing.
- Python Break Continue Make Break Continue click with one clear analogy you can reuse whenever Python feels confusing.
- Python Closures See how Python functions can remember private information, even after the outer function has already finished.