def
The def keyword defines a function in Python. Functions are reusable blocks of code that perform a specific task. They help you organize code, avoid repetition, and build modular programs.
Syntax
def function_name(parameters):
"""Optional docstring describing the function."""
# Function body
return result # Optional
The syntax is minimal, but the real value of functions becomes clear when you see them in action. The examples below progress from the simplest possible function through parameter passing, return values, and then into more advanced patterns that form the foundation of Python’s function system.
Basic Examples
A Simple Function
def greet():
print("Hello, World!")
greet()
# Hello, World!
A function that always prints the same greeting has limited utility. Adding parameters lets callers pass data into the function so it can adapt its behavior based on the arguments provided at each call site. The parameter name acts as a local variable inside the function body, bound to whatever value the caller supplies.
Function with Parameters
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
# Hello, Alice!
Printing output is useful for interactive scripts, but functions that compute and return values are more composable. A return value can be stored in a variable, passed to another function, or used in an expression. Functions without an explicit return statement implicitly return None, which can lead to surprising bugs if the caller expects a value back.
Function with Return Value
def add(a, b):
return a + b
result = add(3, 5)
print(result)
# 8
When a parameter is optional in most use cases, providing a default value in the function signature avoids forcing every caller to supply it. The default is evaluated once at definition time, which is important to remember when the default is a mutable object like a list or dictionary. Default arguments make function interfaces more ergonomic without sacrificing explicitness for callers who need the non-default behavior.
Default Parameters
You can provide default values for parameters:
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
print(greet("Alice"))
# Hello, Alice!
print(greet("Bob", "Hi"))
# Hi, Bob!
When a function takes several parameters, positional arguments can become ambiguous at the call site. Keyword arguments let callers name each value they pass, which makes the intent explicit regardless of parameter order. This is especially valuable for functions with many parameters or boolean flags where True or False alone gives no clue about what is being set.
Keyword Arguments
Pass arguments by name for clarity and flexibility:
def describe_pet(animal, name):
return f"I have a {animal} named {name}."
# Positional arguments
print(describe_pet("dog", "Buddy"))
# I have a dog named Buddy.
# Keyword arguments
print(describe_pet(name="Whiskers", animal="cat"))
# I have a cat named Whiskers.
Sometimes you cannot know in advance how many arguments a function will receive. The *args syntax collects extra positional arguments into a tuple, letting you write functions that accept a variable number of inputs. This pattern appears throughout the standard library, most notably in print() and str.format(), which accept any number of arguments without requiring the caller to wrap them in a list.
*args and **kwargs
Variable Positional Arguments
def sum_all(*args):
total = 0
for num in args:
total += num
return total
print(sum_all(1, 2, 3))
# 6
print(sum_all(10, 20, 30, 40))
# 100
The *args syntax handles positional arguments, but functions often need to accept named arguments whose names are not known in advance. The **kwargs syntax collects those extra keyword arguments into a dictionary, where each key is the argument name as a string and each value is the corresponding argument value. This pattern is essential for writing wrapper functions that forward arbitrary keyword arguments to an inner call, such as decorators or subclass methods that call super().
Variable Keyword Arguments
def print_info(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")
print_info(name="Alice", age=30, city="NYC")
# name: Alice
# age: 30
# city: NYC
When all three parameter kinds appear together, Python enforces a strict order: regular parameters come first, then *args, then keyword-only parameters (those after *args or a bare *), and finally **kwargs. This ordering rule is not just convention; the interpreter enforces it and will raise a SyntaxError if you violate it.
Combining All Types
def func(a, b, *args, default="value", **kwargs):
print(f"a={a}, b={b}")
print(f"args={args}")
print(f"default={default}")
print(f"kwargs={kwargs}")
func(1, 2, 3, 4, default="custom", x=10, y=20)
# a=1, b=2
# args=(3, 4)
# default=custom
# kwargs={x: 10, y: 20}
Type annotations make function signatures self-documenting and enable static analysis tools like mypy to catch type errors before runtime. While annotations are optional and never enforced by the interpreter, they serve as machine-checked documentation that communicates the expected types to other developers and to IDE autocompletion engines.
Type Hints
Python 3.5+ supports type hints:
def greet(name: str) -> str:
return f"Hello, {name}!"
def process_numbers(numbers: list[int]) -> int:
return sum(numbers)
# With None and Optional
from typing import Optional
def find_user(user_id: int, default: Optional[str] = None) -> str:
return default or "Guest"
Type hints describe what a function expects and returns, but docstrings explain why the function exists and how to use it. A well-written docstring covers the purpose, the parameters, the return value, and any side effects or exceptions. The help() built-in and documentation generators like Sphinx rely on docstrings, so writing them is a habit that pays off as soon as another developer (or your future self) encounters the function.
Docstrings
Document your functions with docstrings:
def calculate_area(width, height):
"""Calculate the area of a rectangle.
Args:
width: The width of the rectangle.
height: The height of the rectangle.
Returns:
The area (width * height).
"""
return width * height
Common docstring styles:
- Google style
- NumPy style
- Sphinx (reST) style
Python allows defining functions inside other functions. Inner functions have access to the enclosing scope’s variables through lexical scoping, which makes them useful for encapsulating helper logic that should not be exposed to the module level. The inner function is recreated each time the outer function runs, so it can capture different state on each invocation.
Nested Functions
Functions can be defined inside other functions:
def outer():
x = "outer"
def inner():
print(f"Inner sees: {x}")
inner()
outer()
# Inner sees: outer
A closure is a nested function that remembers variables from its enclosing scope even after the outer function has returned. This pattern provides a lightweight way to create function factories without defining a full class. Each call to the factory produces a new closure with its own captured state, so double and triple in the following example each hold an independent factor value.
Closures
Inner functions that capture variables from their enclosing scope:
def make_multiplier(factor):
def multiplier(number):
return number * factor
return multiplier
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5))
# 10
print(triple(5))
# 15
Lambda functions provide a concise syntax for defining simple functions inline without a formal def statement. They are restricted to a single expression and cannot contain statements, assignments, or annotations. The main use case is passing short behavior-defining functions to higher-order functions like sorted(), filter(), and map(), where a full def would add unnecessary ceremony.
Lambda Functions
Anonymous inline functions:
# Full function
def square(x):
return x ** 2
# Lambda equivalent
square = lambda x: x ** 2
print(square(4))
# 16
# Common use cases
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
sorted_nums = sorted(numbers, key=lambda x: -x)
print(sorted_nums)
# [9, 6, 5, 4, 3, 2, 1, 1]
While regular functions compute a result and return it all at once, generator functions produce values lazily using the yield keyword. Each call to yield pauses the function and hands a value back to the caller, and the function resumes from where it left off when the next value is requested. This lazy evaluation model lets generators handle sequences that would be impractical to store in memory all at once.
Generator Functions
Use yield to create generators:
def count_up_to(n):
"""Generator that yields numbers from 1 to n."""
count = 1
while count <= n:
yield count
count += 1
for num in count_up_to(5):
print(num)
# 1
# 2
# 3
# 4
# 5
Writing functions is straightforward, but writing functions that remain clear and safe as the codebase grows requires a few habits. The following practices address the most common pitfalls that make functions harder to debug, test, and modify over time.
Best Practices
Name Functions Clearly
# Bad
def do_stuff(x):
return x * 2
# Good
def calculate_double(value):
return value * 2
A clear name signals intent, but a function should also stay focused on a single responsibility. When a function tries to validate data, persist it to a database, and send a notification email all at once, testing each concern in isolation becomes difficult and changing one behavior risks breaking another. Splitting such a function into smaller, single-purpose functions makes each piece easier to understand and modify independently.
Keep Functions Short
Each function should do one thing well:
# Bad
def process_user_data(user):
# Validate, save, and send email...
# Good
def validate_user(user):
...
def save_user(user):
...
def send_welcome_email(user):
...
Default parameter values are evaluated once at function definition time, not each time the function is called. When the default is a mutable object like a list or dictionary, modifications made inside one call persist across subsequent calls, which produces confusing bugs that are difficult to trace. Using None as a sentinel and creating a fresh mutable object inside the function body is the standard way to avoid this behavior.
Use Default Arguments Carefully
Avoid mutable defaults:
# Bad - default list persists across calls
def add_item(item, items=[]):
items.append(item)
return items
# Good - use None
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
See Also
- len built-in — get the length of sequences
- range built-in — generate sequences for looping
- enumerate built-in — get index and value during iteration
- map built-in — apply functions to sequences