Python Functions - Complete Guide
Understanding Python Functions
Functions are one of the fundamental building blocks in Python. They allow you to encapsulate a block of code that can be reused throughout your program. Functions help make your code modular, maintainable, and easier to understand.
In Python, functions can be defined with the def
keyword, followed by the function name,
a pair of parentheses which may include parameters, and a colon. The function body is indented.
# Basic function definition def greet(): print("Hello, World!") # Calling the function greet() # Output: Hello, World! # Function with parameters def greet_person(name): print(f"Hello, {name}!") greet_person("Alice") # Output: Hello, Alice! # Function with return value def add_numbers(a, b): return a + b result = add_numbers(5, 3) print(result) # Output: 8
Function Parameters and Arguments
Python offers a flexible parameter system for functions, allowing various ways to pass data to your functions.
Required Parameters
# Function with required parameters def calculate_rectangle_area(length, width): return length * width # Must provide all required arguments area = calculate_rectangle_area(5, 3) print(area) # Output: 15
Default Parameters
# Function with default parameter values def greet(name, greeting="Hello"): return f"{greeting}, {name}!" # Using the default value print(greet("Alice")) # Output: Hello, Alice! # Overriding the default value print(greet("Bob", "Hi")) # Output: Hi, Bob!
Keyword Arguments
# Using keyword arguments def create_profile(name, age, occupation): return f"{name} is {age} years old and works as a {occupation}." # Calling with positional arguments print(create_profile("Alice", 30, "Engineer")) # Calling with keyword arguments (can be in any order) print(create_profile(age=25, name="Bob", occupation="Designer")) # Mixing positional and keyword arguments # Positional arguments must come before keyword arguments print(create_profile("Charlie", occupation="Doctor", age=35))
Variable-Length Arguments (*args and **kwargs)
# *args: Variable number of positional arguments def calculate_sum(*numbers): return sum(numbers) print(calculate_sum(1, 2, 3)) # Output: 6 print(calculate_sum(5, 10, 15, 20)) # Output: 50 # **kwargs: Variable number of keyword arguments def print_person_details(**details): for key, value in details.items(): print(f"{key}: {value}") print_person_details(name="Alice", age=30, city="New York", job="Engineer") # Using both *args and **kwargs def example_function(*args, **kwargs): print(f"Args: {args}") print(f"Kwargs: {kwargs}") example_function(1, 2, 3, name="Alice", age=30)
Return Values
Functions can return values using the return
statement. A function can return a single value,
multiple values, or nothing at all.
# Function returning a single value def square(x): return x ** 2 result = square(5) print(result) # Output: 25 # Function returning multiple values def get_coordinates(): x = 10 y = 20 return x, y # Returns a tuple coords = get_coordinates() print(coords) # Output: (10, 20) # Unpacking multiple return values x, y = get_coordinates() print(f"X: {x}, Y: {y}") # Output: X: 10, Y: 20 # Early returns def get_absolute(number): if number < 0: return -number return number # This line executes only if number >= 0 # Function with no return (returns None implicitly) def say_hello(): print("Hello!") result = say_hello() print(result) # Output: None
Variable Scope and Lifetime
Variables in Python have different scopes (regions where they're accessible) and lifetimes (duration they exist in memory).
Local and Global Scope
# Local variables def function_with_local_var(): local_var = "I'm local" print(local_var) # Works fine function_with_local_var() # print(local_var) # NameError: local_var is not defined # Global variables global_var = "I'm global" def function_using_global(): print(global_var) # Can access global variables function_using_global() # Modifying global variables counter = 0 def increment_counter(): global counter # Declare as global to modify counter += 1 print(counter) increment_counter() # Output: 1 increment_counter() # Output: 2
Nested Functions and Closures
# Nested function def outer_function(x): def inner_function(y): return x + y # inner_function can access outer_function's variables return inner_function # Creating a closure (a function that remembers its context) add_five = outer_function(5) print(add_five(10)) # Output: 15 print(add_five(20)) # Output: 25 # Using nonlocal for nested function variables def counter_function(): count = 0 def increment(): nonlocal count # Use nonlocal to modify outer function's variables count += 1 return count return increment counter = counter_function() print(counter()) # Output: 1 print(counter()) # Output: 2 print(counter()) # Output: 3
Recursive Functions
Recursive functions are functions that call themselves. They can be powerful for solving problems that can be broken down into smaller, similar sub-problems.
# Factorial calculation using recursion def factorial(n): if n <= 1: # Base case return 1 else: # Recursive case return n * factorial(n - 1) print(factorial(5)) # Output: 120 (5 * 4 * 3 * 2 * 1) # Fibonacci sequence using recursion def fibonacci(n): if n <= 0: return 0 elif n == 1: return 1 else: return fibonacci(n - 1) + fibonacci(n - 2) # Print first 10 Fibonacci numbers for i in range(10): print(fibonacci(i), end=" ") # Output: 0 1 1 2 3 5 8 13 21 34
Recursion Considerations
- Always have a base case to stop the recursion
- Python has a default recursion limit (typically 1000) to prevent stack overflow
- Recursive solutions can be less efficient than iterative ones due to function call overhead
- Consider using memoization for recursive functions with overlapping subproblems
Lambda Functions
Lambda functions (sometimes called anonymous functions) are small, one-line functions defined using
the lambda
keyword. They're useful for simple functions that are used only once or as
function arguments.
# Basic lambda function square = lambda x: x ** 2 print(square(5)) # Output: 25 # Multiple parameters add = lambda x, y: x + y print(add(3, 4)) # Output: 7 # Using lambda with built-in functions numbers = [5, 2, 8, 1, 9, 3] # Sort a list using a custom key sorted_numbers = sorted(numbers) print(sorted_numbers) # Output: [1, 2, 3, 5, 8, 9] # Filter to get only even numbers even_numbers = list(filter(lambda x: x % 2 == 0, numbers)) print(even_numbers) # Output: [2, 8] # Map to square each number squared = list(map(lambda x: x ** 2, numbers)) print(squared) # Output: [25, 4, 64, 1, 81, 9]
Function Decorators
Decorators are a powerful feature that allow you to modify the behavior of functions. They are functions that take another function as an argument, extend its behavior, and return the modified function.
# Basic decorator def my_decorator(func): def wrapper(): print("Something is happening before the function is called.") func() # Call the original function print("Something is happening after the function is called.") return wrapper # Using a decorator @my_decorator def say_hello(): print("Hello!") # This is equivalent to: say_hello = my_decorator(say_hello) say_hello() # Output: # Something is happening before the function is called. # Hello! # Something is happening after the function is called. # Decorator for functions with arguments def decorator_with_args(func): def wrapper(*args, **kwargs): print(f"Arguments passed: {args}, {kwargs}") return func(*args, **kwargs) return wrapper @decorator_with_args def add(a, b): return a + b result = add(5, b=3) # Output: Arguments passed: (5,), {'b': 3} print(result) # Output: 8
Practical Decorator Examples
# Timing decorator import time def timer_decorator(func): def wrapper(*args, **kwargs): start_time = time.time() result = func(*args, **kwargs) end_time = time.time() print(f"Function {func.__name__} took {end_time - start_time:.6f} seconds to execute.") return result return wrapper @timer_decorator def slow_function(): time.sleep(1) # Simulating a time-consuming task return "Done!" print(slow_function()) # Decorator with parameters def repeat(n): def decorator(func): def wrapper(*args, **kwargs): results = [] for _ in range(n): results.append(func(*args, **kwargs)) return results return wrapper return decorator @repeat(3) def say_hi(name): return f"Hi, {name}!" print(say_hi("Alice")) # Output: ['Hi, Alice!', 'Hi, Alice!', 'Hi, Alice!']
Function Best Practices
Function Design Principles
- Follow the Single Responsibility Principle: Functions should do one thing and do it well
- Keep functions small and focused
- Use descriptive function names (use verbs for functions that perform actions)
- Limit the number of parameters (consider using classes or dictionaries for many parameters)
- Minimize side effects (changes to variables outside the function)
- Include docstrings to document your functions
Documentation with Docstrings
# Using docstrings to document functions def calculate_area(radius): """ Calculate the area of a circle. Args: radius (float): The radius of the circle. Returns: float: The area of the circle. Raises: ValueError: If radius is negative. """ if radius < 0: raise ValueError("Radius cannot be negative") return 3.14159 * radius ** 2 # Accessing the docstring print(calculate_area.__doc__) # Help function shows the docstring help(calculate_area)
Type Hints (Python 3.5+)
# Using type hints for better documentation and tooling support def greet(name: str, times: int = 1) -> str: """Greet a person multiple times.""" return f"Hello, {name}! " * times # More complex type hints from typing import List, Dict, Optional, Union def process_data( items: List[int], options: Dict[str, str] = {}, callback: Optional[callable] = None ) -> Union[List[int], Dict[str, int]]: """Process a list of integers.""" result = [item * 2 for item in items] if callback: callback(result) if "format" in options and options["format"] == "dict": return {f"item_{i}": item for i, item in enumerate(result)} return result # Type hints don't enforce types at runtime # This will still run (but static type checkers will warn) print(greet(123)) # Works: "Hello, 123!"
Advanced Function Topics
Function as First-Class Objects
# Functions as variables def square(x): return x ** 2 def cube(x): return x ** 3 # Assigning a function to a variable operation = square print(operation(5)) # Output: 25 # Functions as arguments def apply_operation(func, value): return func(value) print(apply_operation(square, 5)) # Output: 25 print(apply_operation(cube, 5)) # Output: 125 # Functions returning functions def get_operation(opcode): if opcode == "square": return square elif opcode == "cube": return cube else: return lambda x: x # Identity function as default operation = get_operation("cube") print(operation(5)) # Output: 125
Function Caching with @lru_cache
# Using memoization to improve recursive functions from functools import lru_cache # Without caching (slow for larger values) def fibonacci_slow(n): if n <= 1: return n return fibonacci_slow(n-1) + fibonacci_slow(n-2) # With caching (much faster) @lru_cache(maxsize=None) def fibonacci_fast(n): if n <= 1: return n return fibonacci_fast(n-1) + fibonacci_fast(n-2) # Compare execution time for larger value import time n = 30 start = time.time() result_slow = fibonacci_slow(n) end = time.time() print(f"Slow: {end - start:.6f} seconds") start = time.time() result_fast = fibonacci_fast(n) end = time.time() print(f"Fast: {end - start:.6f} seconds")
Partial Functions
# Creating new functions by fixing some parameters from functools import partial # Original function def power(base, exponent): return base ** exponent # Create specialized functions square = partial(power, exponent=2) cube = partial(power, exponent=3) print(square(5)) # Output: 25 print(cube(5)) # Output: 125 # Another example with multiple arguments def format_string(string, prefix, suffix): return f"{prefix}{string}{suffix}" # Create a HTML paragraph formatter p_formatter = partial(format_string, prefix="", suffix="
") print(p_formatter("Hello, World!")) # Output:Hello, World!
Practice Exercises
Exercise 1: Function Basics
# Write a function called `calculate_average` that takes a list
# of numbers and returns their average.
# Test it with different lists.
# Your code here
Exercise 2: Recursive Function
# Write a recursive function called `sum_digits` that takes
# a positive integer and returns the sum of its digits.
# For example, sum_digits(123) should return 6 (1+2+3).
# Your code here
Exercise 3: Decorator
# Create a decorator called `logger` that prints the name of the
# function being called, the arguments it received, and its return value.
# Your code here