📝 Python

Pure Functions

P
Author
Pyland
📅
Published
03.04.2026
⏱️
Reading time
4 min
👁️
Views
191
🌳
Level
Advanced

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.

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

📝

Event Loop in Python: How asyncio Achieves "Paral…

Event loop is the heart of asyncio. It doesn't run code in parallel across multiple...

📅 30.06.2026 👁️ 136
📝

pytest-django: Testing Django

Охватываемые темы: Installation, @pytest.mark.djangodb, Fixtures, Testing views.

📅 30.06.2026 👁️ 144
📝

pip: Python Package Manager

Охватываемые темы: Installing packages, Upgrading and removing, requirements.txt, Virtual environment.

📅 30.06.2026 👁️ 139

Did you like the article?

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