The key rule: pass state through arguments, return new data, never modify existing data. Isolate impure operations (I/O, database, network) into dedicated functions.
What Is a Pure Function?
A pure function is a function that:
1. Always returns the same result for the same arguments
2. Has no side effects — it does not modify external state
# Pure function
def add(a, b):
return a + b
print(add(2, 3)) # 5
print(add(2, 3)) # 5 — always the same
# Impure function
total = 0
def add_impure(x):
global total
total += x # modifies external state!
return total
print(add_impure(5)) # 5
print(add_impure(5)) # 10 — different result!
Examples of Pure Functions
# Math — pure
def square(x):
return x ** 2
def average(numbers):
return sum(numbers) / len(numbers)
def distance(x1, y1, x2, y2):
return ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
# Strings — pure (original string is never changed)
def uppercase(text):
return text.upper()
def count_vowels(text):
vowels = "aeiouAEIOU"
return sum(1 for char in text if char in vowels)
# Lists — pure (returns a NEW list)
def double_all(numbers):
return [x * 2 for x in numbers]
def filter_positive(numbers):
return [x for x in numbers if x > 0]
Examples of Impure Functions
# 1. Modifying global state
total_score = 0
def add_score_impure(points): # ❌ impure
global total_score
total_score += points
return total_score
def add_score_pure(current_total, points): # ✅ pure
return current_total + points
# 2. Mutating arguments
def append_impure(lst, item): # ❌ mutates the original list!
lst.append(item)
return lst
def append_pure(lst, item): # ✅ returns a new list
return lst + [item]
# 3. Side effects (I/O)
def print_result_impure(x): # ❌ print is a side effect
print(f"Result: {x}")
return x
def format_result_pure(x): # ✅ returns a string — printing happens outside
return f"Result: {x}"
# 4. Randomness
import random
def get_random_impure(): # ❌ different result every time
return random.randint(1, 100)
def get_random_pure(seed): # ✅ same result for the same seed
random.seed(seed)
return random.randint(1, 100)
Why Pure Functions Are Great
Predictability and testability
def add(a, b):
return a + b
# Testing is simple and reliable
def test_add():
assert add(2, 3) == 5
assert add(0, 0) == 0
assert add(-1, 1) == 0
# No setUp/tearDown needed — works the same every time
Memoization (caching)
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
"""Pure function — safe to cache."""
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(100)) # computed
print(fibonacci(100)) # returned from cache instantly
Safe parallelism
from multiprocessing import Pool
def square(x):
return x ** 2
with Pool(4) as pool:
results = pool.map(square, [1, 2, 3, 4, 5])
print(results) # [1, 4, 9, 16, 25]
# No shared state — no data races
How to Make a Function Pure
Problem: global variable → pass it as an argument
# ❌ impure
score = 0
def add_points_impure(points):
global score
score += points
return score
# ✅ pure
def add_points_pure(current_score, points):
return current_score + points
score = 0
score = add_points_pure(score, 10) # 10
score = add_points_pure(score, 5) # 15
Problem: mutating a collection → return a new one
# ❌ impure — mutates the original
def sort_impure(numbers):
numbers.sort()
return numbers
# ✅ pure — returns a new list
def sort_pure(numbers):
return sorted(numbers)
original = [3, 1, 2]
sorted_list = sort_pure(original)
print(original) # [3, 1, 2] ← unchanged
print(sorted_list) # [1, 2, 3]
# ❌ impure — mutates the dict
def update_age_impure(person, new_age):
person["age"] = new_age
return person
# ✅ pure — returns a new dict
def update_age_pure(person, new_age):
return {**person, "age": new_age}
person = {"name": "Alice", "age": 25}
updated = update_age_pure(person, 26)
print(person) # {'name': 'Alice', 'age': 25} ← unchanged
print(updated) # {'name': 'Alice', 'age': 26}
Practical Example: Processing Student Data
def get_high_performers(students, threshold=90):
"""Pure: filter by grade."""
return [s for s in students if s["grade"] >= threshold]
def add_status(students):
"""Pure: adds a field without mutating the original."""
return [
{**s, "status": "Honors" if s["grade"] >= 90 else "Passing"}
for s in students
]
students = [
{"name": "Alice", "grade": 95},
{"name": "Bob", "grade": 87},
{"name": "Charlie", "grade": 92},
]
high_performers = get_high_performers(students, 90)
with_status = add_status(students)
print(high_performers)
# [{'name': 'Alice', 'grade': 95}, {'name': 'Charlie', 'grade': 92}]
print(students[0])
# {'name': 'Alice', 'grade': 95} ← 'status' field not added to original
Common Mistakes
Mistake 1: Mutating an argument directly
# ❌ mutates the list — callers don't expect this
def add_item_impure(items, new_item):
items.append(new_item)
return items
# ✅ create a new list
def add_item_pure(items, new_item):
return items + [new_item]
Mistake 2: Hidden dependency on time or randomness
import time, random
# ❌ impure — results are unpredictable
def get_timestamp():
return time.time()
def shuffle_data(items):
random.shuffle(items) # also mutates!
return items
# ✅ receive the value as an argument
def format_timestamp(timestamp):
return time.strftime("%Y-%m-%d", time.localtime(timestamp))
The key rule: pass state through arguments, return new data, never modify existing data. Isolate impure operations (I/O, database, network) into dedicated functions.
💬 Comments (0)
No comments yet
Be the first to share your opinion about this article!