📝 Python

Pure Functions — Clean and Predictable! 🧼

0
Author
04e5cc8b-58ac-4bdc-bdee-661bbb
📅
Published
03.04.2026
⏱️
Reading time
8 min
👁️
Views
112
🌱
Level
Beginner

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 outputprint() is a side effect
  • 🌐 Network requests — API calls are impure
  • Working with timetime.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! 🧼✨

Your reaction to the article

💬 Comments (0)

🔐 Sign in to leave a comment
🚪 Login
💭

No comments yet

Be the first to share your opinion about this article!

🔗 Similar

Similar articles

Continue learning with these materials

📝

Setting Up Your Environment: Python, pip, and VS …

Before writing code locally, you need to set up three tools: Python, pip, and VS...

📅 04.06.2026 👁️ 17
📝

The datetime Module: Working with Dates and Times

datetime is Python's standard module for working with dates and times. It's part of the...

📅 08.05.2026 👁️ 67
📝

.env Files and Environment Variables: Keeping Sec…

Imagine you wrote a program with an API key hardcoded in the source and pushed...

📅 08.05.2026 👁️ 76

Did you like the article?

Subscribe to our updates and receive new articles first. Grow with PyLand!