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 (does not modify external state)
Example of a PURE function:
def add(a, b):
return a + b
print(add(2, 3)) # 5
print(add(2, 3)) # 5 ← Always the same result!
Example of an IMPURE function:
total = 0 # Global variable
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!
Pure functions are predictable and reliable! 🎯
Characteristics of a Pure Function
✅ Pure function:
def multiply(x, y):
"""Pure: only arguments → result."""
return x * y
- ✅ Uses only its own arguments
- ✅ Does not read global variables
- ✅ Does not mutate arguments
- ✅ Does not print or write to files
- ✅ Same input → same output every time
❌ Impure function:
counter = 0
def increment_impure():
"""Impure: modifies a global variable."""
global counter
counter += 1
return counter
- ❌ Reads global variables
- ❌ Modifies external state
- ❌ Different result on each call
Examples of pure functions
1. Math operations
def square(x):
"""Square a number."""
return x ** 2
def average(numbers):
"""Arithmetic mean."""
return sum(numbers) / len(numbers)
def distance(x1, y1, x2, y2):
"""Euclidean distance between two points."""
return ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
All pure: same arguments → same result every time!
2. String operations
def uppercase(text):
"""Convert to uppercase."""
return text.upper()
def reverse(text):
"""Reverse a string."""
return text[::-1]
def count_vowels(text):
"""Count vowels."""
vowels = "aeiouAEIOU"
return sum(1 for char in text if char in vowels)
All pure: the original string is never changed!
3. List operations
def double_all(numbers):
"""Double every number."""
return [x * 2 for x in numbers]
def filter_positive(numbers):
"""Keep only positive numbers."""
return [x for x in numbers if x > 0]
def get_first_n(data, n):
"""Return the first N elements."""
return data[:n]
All pure: return a NEW list, leaving the original untouched!
Examples of impure functions
1. Modifying global state
# ❌ IMPURE
total_score = 0
def add_score_impure(points):
global total_score
total_score += points
return total_score
# ✅ PURE
def add_score_pure(current_total, points):
return current_total + points
2. Mutating arguments
# ❌ IMPURE
def append_impure(lst, item):
lst.append(item) # Mutates the original list!
return lst
# ✅ PURE
def append_pure(lst, item):
return lst + [item] # Returns a new list
3. Side effects (I/O)
# ❌ IMPURE
def save_to_file_impure(data):
with open("data.txt", "w") as f:
f.write(data)
return True
# ❌ IMPURE
def print_result_impure(x):
print(f"Result: {x}") # print is a side effect!
return x
# ✅ PURE (returns data, printing happens outside)
def format_result_pure(x):
return f"Result: {x}"
4. Using randomness
import random
# ❌ IMPURE
def get_random_number_impure():
return random.randint(1, 100)
# ✅ PURE (seed is passed as an argument)
def get_random_number_pure(seed):
random.seed(seed)
return random.randint(1, 100)
Why are pure functions great?
1. Predictability
# Pure function
def add(a, b):
return a + b
# Always the same result!
assert add(2, 3) == 5
assert add(2, 3) == 5
assert add(2, 3) == 5
2. Easy to test
# Testing a pure function is straightforward!
def test_add():
assert add(2, 3) == 5
assert add(0, 0) == 0
assert add(-1, 1) == 0
# Always works the same way!
3. Safe to parallelize
# Pure functions can be called in parallel safely!
from multiprocessing import Pool
def square(x):
return x ** 2
numbers = [1, 2, 3, 4, 5]
with Pool(4) as pool:
results = pool.map(square, numbers)
print(results) # [1, 4, 9, 16, 25]
4. 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)
# Repeated calls are instant!
print(fibonacci(100)) # Computed
print(fibonacci(100)) # Returned from cache!
How to make a function pure?
Problem 1: Global variable
# ❌ IMPURE
score = 0
def add_points_impure(points):
global score
score += points
return score
# ✅ PURE — pass state as an argument
def add_points_pure(current_score, points):
return current_score + points
# Usage
score = 0
score = add_points_pure(score, 10) # 10
score = add_points_pure(score, 5) # 15
Problem 2: Mutating a list
# ❌ IMPURE
def sort_impure(numbers):
numbers.sort() # Mutates the original!
return numbers
# ✅ PURE — return a new list
def sort_pure(numbers):
return sorted(numbers) # New list
# Usage
original = [3, 1, 2]
sorted_list = sort_pure(original)
print(original) # [3, 1, 2] ← unchanged
print(sorted_list) # [1, 2, 3]
Problem 3: Mutating a dictionary
# ❌ IMPURE
def update_age_impure(person, new_age):
person["age"] = new_age # Mutates the original!
return person
# ✅ PURE — return a new dictionary
def update_age_pure(person, new_age):
return {**person, "age": new_age}
# Usage
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 examples
Example 1: Processing student data
# Pure function — filtering
def get_high_performers(students, threshold=90):
"""Students with grade >= threshold."""
return [s for s in students if s["grade"] >= threshold]
# Pure function — transformation
def add_status(students):
"""Add a status field without mutating the original."""
return [
{**s, "status": "Honors" if s["grade"] >= 90 else "Passing"}
for s in students
]
# Usage
students = [
{"name": "Alice", "grade": 95},
{"name": "Bob", "grade": 87},
{"name": "Carl", "grade": 92}
]
high_performers = get_high_performers(students, 90)
with_status = add_status(students)
# Original is NOT changed!
print(students) # No "status" field
Example 2: ML model metrics
# Pure functions — math operations
def calculate_accuracy(correct, total):
"""Model accuracy."""
return (correct / total) * 100 if total > 0 else 0
def calculate_f1_score(precision, recall):
"""F1 score."""
if precision + recall == 0:
return 0
return 2 * (precision * recall) / (precision + recall)
def normalize_scores(scores):
"""Normalize to 0–100."""
max_score = max(scores) if scores else 1
return [s / max_score * 100 for s in scores]
# Usage
acc = calculate_accuracy(85, 100) # 85.0
f1 = calculate_f1_score(0.9, 0.85) # 0.874
normalized = normalize_scores([10, 50, 75, 100]) # [10, 50, 75, 100]
Example 3: Price calculations
# Pure functions for prices
def apply_discount(price, discount_percent):
"""Apply a discount."""
return price * (1 - discount_percent / 100)
def add_tax(price, tax_percent):
"""Add tax."""
return price * (1 + tax_percent / 100)
def calculate_total(prices):
"""Sum of all prices."""
return sum(prices)
# Composing pure functions
def final_price(base_price, discount, tax):
"""Final price: discount → tax."""
discounted = apply_discount(base_price, discount)
with_tax = add_tax(discounted, tax)
return round(with_tax, 2)
# Usage
price = final_price(100, 20, 10) # 100 → 80 (discount) → 88 (tax)
print(price) # 88.0
Composing pure functions
Pure functions compose like LEGO bricks!
# A set of pure functions
def double(x):
return x * 2
def add_ten(x):
return x + 10
def square(x):
return x ** 2
# Compose them
def compose(*functions):
"""Apply functions in sequence."""
def combined(x):
result = x
for func in functions:
result = func(result)
return result
return combined
# Build a pipeline
pipeline = compose(double, add_ten, square)
print(pipeline(5)) # 5 → 10 (double) → 20 (add_ten) → 400 (square)
Pure vs Impure — comparison
Task: add 10 to every number
Impure approach:
def add_ten_impure(numbers):
for i in range(len(numbers)):
numbers[i] += 10 # Mutates the original!
return numbers
nums = [1, 2, 3]
result = add_ten_impure(nums)
print(nums) # [11, 12, 13] ← changed!
print(result) # [11, 12, 13]
Pure approach:
def add_ten_pure(numbers):
return [n + 10 for n in numbers]
nums = [1, 2, 3]
result = add_ten_pure(nums)
print(nums) # [1, 2, 3] ← unchanged!
print(result) # [11, 12, 13]
Common mistakes
Mistake 1: Mutating an argument directly
# ❌ IMPURE
def add_item_impure(items, new_item):
items.append(new_item)
return items
# ✅ PURE
def add_item_pure(items, new_item):
return items + [new_item]
Mistake 2: Using time or randomness
import time
import random
# ❌ IMPURE (result depends on the current time)
def get_timestamp_impure():
return time.time()
# ❌ IMPURE (result is random)
def shuffle_impure(items):
random.shuffle(items)
return items
# ✅ PURE (receive the value as an argument)
def format_timestamp_pure(timestamp):
return time.strftime("%Y-%m-%d", time.localtime(timestamp))
Mistake 3: Hidden state mutation
class Counter:
def __init__(self):
self.count = 0
# ❌ IMPURE (modifies the object)
def increment_impure(self):
self.count += 1
return self.count
# ✅ PURE (returns a new value)
def increment_pure(self, current_count):
return current_count + 1
When pure functions are NOT the answer
Pure functions are great — but not always possible!
When impurity is unavoidable:
- 📁 File I/O — reading/writing is inherently a side effect
- 🖥️ Database access — queries change state
- 🖨️ Screen output —
print()is a side effect - 🌐 Network requests — API calls are impure
- ⏰ Working with time —
time.time()is always different
Solution: isolate impurity
# ❌ Mixed: pure + impure in one function
def process_and_save_impure(data):
processed = [x * 2 for x in data] # Pure
with open("result.txt", "w") as f: # Impure
f.write(str(processed))
return processed
# ✅ Separate: pure logic and impure I/O
def process_pure(data):
"""Pure transformation."""
return [x * 2 for x in data]
def save_to_file(data, filename):
"""Impure I/O, isolated."""
with open(filename, "w") as f:
f.write(str(data))
# Usage
data = [1, 2, 3]
processed = process_pure(data) # Pure
save_to_file(processed, "result.txt") # Impure
Summary
A pure function:
- ✅ Same arguments → same result, always
- ✅ Does not modify external state
- ✅ Does not read global variables
- ✅ Has no side effects (no I/O, no print)
Benefits:
- 🎯 Predictability
- 🧪 Easy to test
- 🔄 Safe to cache
- ⚡ Safe to parallelize
- 🧩 Composable
How to make a function pure:
# Pass state as an argument
def add_points(current_score, points):
return current_score + points
# Return new data, don't mutate old data
def update_list(items, new_item):
return items + [new_item]
# Isolate impure operations
processed = process_pure(data) # Pure
save_to_file(processed) # Impure, separate
What’s next?
Now you know pure functions! 🎉
Next topics:
- Data pipelines — chains of pure functions
- Function composition — combining functions
- Immutability — working with immutable data
Pure functions are the foundation of reliable code! 🧼✨
💬 Comments (0)
No comments yet
Be the first to share your opinion about this article!