Functions & Scope
Define reusable functions, understand variable scope, and leverage advanced parameter handling with *args and **kwargs.
Defining Functions
Functions in Python are defined using the def keyword followed by a name and parentheses. They allow you to encapsulate reusable logic and improve code organization. Every function returns a value; if no explicit return statement is provided, the function returns None. Docstrings placed immediately after the function definition serve as built-in documentation accessible via the help() function.
# Basic function definition
def greet(name):
"""Return a greeting message for the given name."""
return f"Hello, {name}!"
print(greet("Alice")) # "Hello, Alice!"
# Function with multiple return values
def divide(a, b):
"""Return quotient and remainder."""
quotient = a // b
remainder = a % b
return quotient, remainder
q, r = divide(17, 5)
print(f"17 / 5 = {q} remainder {r}") # 17 / 5 = 3 remainder 2
# Accessing docstrings
print(greet.__doc__) # "Return a greeting message for the given name."
Arguments and Default Values
Python functions support positional arguments, keyword arguments, and default parameter values. Default values are evaluated once at function definition time, so mutable defaults like lists can lead to unexpected behavior. Keyword arguments allow you to pass values by name, making function calls more readable. You can mix positional and keyword arguments, but positional arguments must come first.
# Default parameter values
def power(base, exponent=2):
return base ** exponent
print(power(3)) # 9
print(power(3, 3)) # 27
# Keyword arguments
def create_user(name, age, role="viewer"):
return {"name": name, "age": age, "role": role}
user = create_user(age=30, name="Bob", role="admin")
print(user) # {'name': 'Bob', 'age': 30, 'role': 'admin'}
# Avoiding mutable default arguments
def append_item(item, target=None):
if target is None:
target = []
target.append(item)
return target
print(append_item(1)) # [1]
print(append_item(2)) # [2] (not [1, 2])
Variable-Length Arguments
Python provides *args and **kwargs to handle functions that accept a variable number of arguments. The *args parameter collects extra positional arguments into a tuple, while **kwargs collects extra keyword arguments into a dictionary. These features are widely used in decorators, wrapper functions, and APIs that need to forward arguments. You can combine regular parameters with *args and **kwargs in the same function signature.
# *args for variable positional arguments
def calculate_sum(*args):
total = 0
for num in args:
total += num
return total
print(calculate_sum(1, 2, 3, 4, 5)) # 15
# **kwargs for variable keyword arguments
def build_profile(**kwargs):
profile = {}
for key, value in kwargs.items():
profile[key] = value
return profile
print(build_profile(name="Alice", age=30, city="NYC"))
# Combining all parameter types
def flexible(required, *args, default=10, **kwargs):
print(f"required: {required}")
print(f"args: {args}")
print(f"default: {default}")
print(f"kwargs: {kwargs}")
flexible("hello", 1, 2, 3, default=20, x=1, y=2)
Lambda Functions and Scope
Lambda functions are anonymous, single-expression functions defined with the lambda keyword. They are commonly used as arguments to higher-order functions like map(), filter(), and sorted(). Python follows the LEGB rule for variable scope: Local, Enclosing, Global, Built-in. Understanding scope is crucial for avoiding naming conflicts and writing predictable code.
# Lambda functions
square = lambda x: x ** 2
print(square(5)) # 25
# Lambda with sorted
students = [("Alice", 88), ("Bob", 75), ("Charlie", 92)]
sorted_students = sorted(students, key=lambda s: s[1], reverse=True)
print(sorted_students) # [('Charlie', 92), ('Alice', 88), ('Bob', 75)]
# Map and filter with lambda
numbers = [1, 2, 3, 4, 5, 6, 7, 8]
doubled = list(map(lambda x: x * 2, numbers))
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(doubled) # [2, 4, 6, 8, 10, 12, 14, 16]
print(evens) # [2, 4, 6, 8]
# Scope demonstration (LEGB)
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x) # "local"
inner()
print(x) # "enclosing"
outer()
print(x) # "global"