Python - Functions Tutorial (Complete Guide)
• Functions in Python are first-class objects that can be passed as arguments, returned from other functions, and assigned to variables, enabling powerful functional programming patterns
Key Insights
• Functions in Python are first-class objects that can be passed as arguments, returned from other functions, and assigned to variables, enabling powerful functional programming patterns • Python supports multiple parameter types including positional, keyword, default, variable-length (*args), and keyword variable-length (**kwargs) arguments for maximum flexibility • Understanding scope rules (LEGB: Local, Enclosing, Global, Built-in), closures, and decorators is essential for writing maintainable Python code
Function Basics and Definition
Functions in Python are defined using the def keyword followed by the function name and parameters. Here’s the fundamental syntax:
def greet(name):
return f"Hello, {name}!"
result = greet("Alice")
print(result) # Output: Hello, Alice!
Functions can return multiple values using tuple unpacking:
def get_user_info():
return "John", 30, "Engineer"
name, age, profession = get_user_info()
print(f"{name} is {age} years old and works as a {profession}")
Without an explicit return statement, functions return None:
def log_message(msg):
print(f"LOG: {msg}")
result = log_message("System started")
print(result) # Output: None
Parameter Types and Arguments
Python provides multiple ways to define and pass function parameters.
Positional and Keyword Arguments:
def create_profile(name, age, city):
return {"name": name, "age": age, "city": city}
# Positional arguments
profile1 = create_profile("Alice", 25, "NYC")
# Keyword arguments
profile2 = create_profile(city="LA", name="Bob", age=30)
# Mixed (positional must come first)
profile3 = create_profile("Charlie", age=35, city="Chicago")
Default Parameters:
def connect_database(host, port=5432, timeout=30):
return f"Connecting to {host}:{port} (timeout: {timeout}s)"
print(connect_database("localhost"))
print(connect_database("localhost", 3306))
print(connect_database("localhost", timeout=60))
*Variable-Length Arguments (args):
def calculate_sum(*numbers):
return sum(numbers)
print(calculate_sum(1, 2, 3)) # Output: 6
print(calculate_sum(10, 20, 30, 40)) # Output: 100
def log_event(level, *messages):
print(f"[{level}]", " ".join(messages))
log_event("ERROR", "Database", "connection", "failed")
**Keyword Variable-Length Arguments (kwargs):
def build_query(**conditions):
clauses = [f"{key}={value}" for key, value in conditions.items()]
return "SELECT * FROM users WHERE " + " AND ".join(clauses)
query = build_query(status="active", age=25, city="Boston")
print(query)
Combining All Parameter Types:
def advanced_function(pos1, pos2, *args, kwarg1="default", **kwargs):
print(f"Positional: {pos1}, {pos2}")
print(f"Args: {args}")
print(f"Keyword: {kwarg1}")
print(f"Kwargs: {kwargs}")
advanced_function(1, 2, 3, 4, kwarg1="custom", extra="value", another="param")
Scope and the LEGB Rule
Python resolves variable names using the LEGB rule: Local, Enclosing, Global, Built-in.
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(f"Local x: {x}")
inner()
print(f"Enclosing x: {x}")
outer()
print(f"Global x: {x}")
# Output:
# Local x: local
# Enclosing x: enclosing
# Global x: global
Using global and nonlocal:
counter = 0
def increment_global():
global counter
counter += 1
increment_global()
print(counter) # Output: 1
def outer_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
print(increment()) # 1
print(increment()) # 2
outer_counter()
First-Class Functions and Higher-Order Functions
Functions in Python are objects that can be assigned, passed, and returned.
def multiply(x, y):
return x * y
def apply_operation(func, a, b):
return func(a, b)
result = apply_operation(multiply, 5, 3)
print(result) # Output: 15
# Storing functions in data structures
operations = {
'add': lambda x, y: x + y,
'subtract': lambda x, y: x - y,
'multiply': multiply
}
print(operations['add'](10, 5)) # 15
print(operations['multiply'](10, 5)) # 50
Returning Functions:
def create_multiplier(factor):
def multiplier(x):
return x * factor
return multiplier
times_three = create_multiplier(3)
times_five = create_multiplier(5)
print(times_three(10)) # 30
print(times_five(10)) # 50
Lambda Functions
Lambda functions are anonymous, single-expression functions useful for short operations.
# Basic lambda
square = lambda x: x ** 2
print(square(5)) # 25
# Lambda with multiple arguments
add = lambda x, y: x + y
print(add(3, 7)) # 10
# Practical use in sorting
users = [
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25},
{"name": "Charlie", "age": 35}
]
sorted_users = sorted(users, key=lambda u: u['age'])
print([u['name'] for u in sorted_users]) # ['Bob', 'Alice', 'Charlie']
# With map and filter
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x ** 2, numbers))
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(squared) # [1, 4, 9, 16, 25]
print(evens) # [2, 4]
Closures and Decorators
Closures are functions that remember values from their enclosing scope.
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
c1 = make_counter()
c2 = make_counter()
print(c1()) # 1
print(c1()) # 2
print(c2()) # 1 (separate closure)
Decorators:
def timer_decorator(func):
import time
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.4f} seconds")
return result
return wrapper
@timer_decorator
def slow_function():
import time
time.sleep(1)
return "Done"
slow_function()
# Decorator with arguments
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Hello, {name}!")
greet("Alice") # Prints 3 times
Type Hints and Documentation
Type hints improve code readability and enable static type checking.
from typing import List, Dict, Optional, Union, Callable
def process_data(
items: List[int],
multiplier: float = 1.0,
filter_func: Optional[Callable[[int], bool]] = None
) -> List[float]:
"""
Process a list of integers with optional filtering.
Args:
items: List of integers to process
multiplier: Factor to multiply each item by
filter_func: Optional function to filter items
Returns:
List of processed float values
"""
if filter_func:
items = [x for x in items if filter_func(x)]
return [x * multiplier for x in items]
result = process_data([1, 2, 3, 4, 5], 2.5, lambda x: x > 2)
print(result) # [7.5, 10.0, 12.5]
def get_config(key: str) -> Union[str, int, None]:
config: Dict[str, Union[str, int]] = {
"host": "localhost",
"port": 8080
}
return config.get(key)
Practical Patterns
Memoization for Performance:
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n: int) -> int:
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(100)) # Computed efficiently
Partial Function Application:
from functools import partial
def power(base, exponent):
return base ** exponent
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(5)) # 25
print(cube(5)) # 125
Generator Functions:
def read_large_file(filepath: str):
with open(filepath) as f:
for line in f:
yield line.strip()
# Memory-efficient processing
# for line in read_large_file('large.txt'):
# process(line)
Functions are the fundamental building blocks of Python programs. Master these patterns to write clean, efficient, and maintainable code.