Python Pure Functions
Introduction
Pure functions are a fundamental concept in functional programming. A pure function is a function that always produces the same output for the same input and has no side effects. In other words, pure functions don't modify anything outside their scope and don't depend on anything that might change outside their scope.
This lesson will introduce you to pure functions in Python, explain their benefits, and demonstrate how to implement them in your code.
What Makes a Function "Pure"?
A pure function has two key characteristics:
- Deterministic: Given the same input, it always returns the same output.
- No Side Effects: It doesn't modify variables outside its scope, doesn't modify its input arguments, doesn't perform I/O operations, and doesn't call other impure functions.
Let's compare pure and impure functions to understand the difference:
Example 1: Pure vs. Impure Functions
# Pure function
def add(a, b):
return a + b
# Impure function - has a side effect (printing)
def add_and_print(a, b):
result = a + b
print(f"The result is {result}") # Side effect: I/O operation
return result
When we call add(3, 4)
, we always get 7
without any side effects. However, add_and_print(3, 4)
both returns 7
and prints to the console, which is a side effect.
Example 2: Pure vs. Impure with State
# Impure function - depends on external state
total = 0
def add_to_total(value):
global total
total += value # Modifies external state
return total
# Pure alternative
def add_to_value(current_total, value):
return current_total + value
The add_to_total
function is impure because it modifies a global variable. The add_to_value
function is pure because it doesn't modify anything outside its scope.
Benefits of Pure Functions
Using pure functions offers several advantages:
- Easier to Test: Because pure functions always return the same output for the same input, they are predictable and easy to test.
- Easier to Debug: When something goes wrong, you only need to check the function's inputs to understand why.
- Concurrency-Friendly: Pure functions don't share state, making them safer for concurrent or parallel execution.
- Memoization: Results can be cached because the same inputs always produce the same output.
- Simpler Reasoning: It's easier to understand code composed of pure functions.
Common Patterns for Pure Functions
Avoiding Mutation of Input Arguments
Instead of modifying input arguments, return new values:
# Impure: modifies the input list
def add_item_impure(my_list, item):
my_list.append(item) # Modifies my_list
return my_list
# Pure: returns a new list
def add_item_pure(my_list, item):
return my_list + [item] # Creates and returns a new list
# Example usage
original = [1, 2, 3]
# Impure function changes original list
result_impure = add_item_impure(original, 4)
print(original) # Output: [1, 2, 3, 4]
print(result_impure) # Output: [1, 2, 3, 4]
print(original is result_impure) # Output: True - they're the same object
# Reset for pure example
original = [1, 2, 3]
# Pure function doesn't change original list
result_pure = add_item_pure(original, 4)
print(original) # Output: [1, 2, 3]
print(result_pure) # Output: [1, 2, 3, 4]
print(original is result_pure) # Output: False - different objects
Working with Dictionaries
Similar to lists, we want to avoid mutating dictionaries:
# Impure: modifies the input dictionary
def update_user_impure(user, key, value):
user[key] = value # Modifies user
return user
# Pure: returns a new dictionary
def update_user_pure(user, key, value):
return {**user, key: value} # Creates a new dict with updated value
# Example
user = {"name": "Alice", "age": 30}
# Impure modification
updated_impure = update_user_impure(user, "age", 31)
print(user) # Output: {'name': 'Alice', 'age': 31}
print(user is updated_impure) # Output: True - same object
# Reset for pure example
user = {"name": "Alice", "age": 30}
# Pure modification
updated_pure = update_user_pure(user, "age", 31)
print(user) # Output: {'name': 'Alice', 'age': 30}
print(updated_pure) # Output: {'name': 'Alice', 'age': 31}
print(user is updated_pure) # Output: False - different objects
Real-World Applications
Data Processing Pipeline
Pure functions are great for data processing pipelines, where each function performs a specific transformation:
def extract_names(data):
return [item["name"] for item in data]
def to_uppercase(names):
return [name.upper() for name in names]
def add_greeting(names):
return [f"Hello, {name}!" for name in names]
# Example data
user_data = [
{"id": 1, "name": "alice", "age": 30},
{"id": 2, "name": "bob", "age": 25},
{"id": 3, "name": "charlie", "age": 35}
]
# Process data through pure function pipeline
names = extract_names(user_data)
uppercase_names = to_uppercase(names)
greetings = add_greeting(uppercase_names)
print(greetings)
# Output: ['Hello, ALICE!', 'Hello, BOB!', 'Hello, CHARLIE!']
# Can also be composed into a single pipeline
result = add_greeting(to_uppercase(extract_names(user_data)))
print(result)
# Output: ['Hello, ALICE!', 'Hello, BOB!', 'Hello, CHARLIE!']
Financial Calculations
Pure functions are ideal for financial calculations where consistency and reproducibility are essential:
def calculate_tax(amount, rate):
return amount * rate
def add_tax_to_items(items, tax_rate):
return [
{**item, "total_price": item["price"] + calculate_tax(item["price"], tax_rate)}
for item in items
]
def calculate_cart_total(items):
return sum(item["total_price"] for item in items)
# Shopping cart
cart = [
{"name": "Book", "price": 20.0},
{"name": "Pen", "price": 5.0},
{"name": "Notebook", "price": 15.0}
]
# Apply tax calculation
tax_rate = 0.08 # 8% tax
cart_with_tax = add_tax_to_items(cart, tax_rate)
total = calculate_cart_total(cart_with_tax)
print(cart_with_tax)
# Output: [
# {'name': 'Book', 'price': 20.0, 'total_price': 21.6},
# {'name': 'Pen', 'price': 5.0, 'total_price': 5.4},
# {'name': 'Notebook', 'price': 15.0, 'total_price': 16.2}
# ]
print(f"Total: ${total:.2f}")
# Output: Total: $43.20
When Pure Functions Aren't Practical
While pure functions have many benefits, there are situations where they aren't practical:
- I/O Operations: Reading/writing files, network requests, database operations
- User Interface Updates: Modifying the UI directly
- Logging: Writing logs inherently has side effects
- Performance-Critical Code: Creating new copies of data structures might be less efficient
In these cases, it's best to:
- Isolate impure code as much as possible
- Keep impure functions simple and focused
- Use pure functions for the majority of your logic
Practical Tips for Writing Pure Functions
- Pass all required data as parameters: Don't rely on global or external state.
- Return new data structures instead of modifying inputs: Use techniques like list/dict comprehensions, slicing, or unpacking.
- Keep functions focused on a single task: Smaller functions are easier to keep pure.
- Use immutable data structures when possible:
tuple
instead oflist
,frozenset
instead ofset
.
Summary
Pure functions are a powerful concept in functional programming that leads to more predictable, testable, and maintainable code. By ensuring that functions always produce the same output for the same input and don't have side effects, you can create code that is easier to reason about and less prone to bugs.
Key takeaways:
- Pure functions always return the same output for the same input
- Pure functions have no side effects
- Pure functions make code easier to test, debug, and reason about
- When working with data structures, return new copies rather than modifying the originals
Exercises
-
Identify which of the following functions are pure and which are impure:
pythondef double(x):
return x * 2
def append_to_list(lst, item):
lst.append(item)
return lst
def get_random_number():
import random
return random.randint(1, 10)
def calculate_area(radius):
return 3.14 * radius ** 2 -
Convert the following impure functions to pure alternatives:
python# 1. Convert this impure function
def modify_dict(d, key, value):
d[key] = value
return d
# 2. Convert this impure function
counter = 0
def increment_counter():
global counter
counter += 1
return counter -
Create a pure function pipeline that processes a list of numbers by:
- Filtering out odd numbers
- Doubling the remaining numbers
- Calculating the sum of the results
Additional Resources
- Functional Programming HOWTO in the Python documentation
- Immutable Data Structures in Python
- Book: "Functional Programming in Python" by David Mertz
Happy coding with pure functions!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)