Skip to main content
Clean Code 60 XP · 7 min

Pure Functions

A pure function always returns the same output for the same input and changes nothing outside itself.

Showing
Ad (728×90)

What Makes a Function Pure

A pure function has two properties: (1) deterministic — same input always produces the same output, (2) no side effects — it doesn't modify anything outside its scope: no globals, no database writes, no I/O, no mutations of its arguments. Pure functions are trivial to test (no setup, no mocks), safe to parallelize, and easy to reason about in isolation.

Impure — depends on external state

discount = 0.10  // global state

function calculateTotal(price, qty):
    // depends on external variable — not deterministic
    return price * qty * (1 - discount)

discount = 0.20  // change this → function behaves differently

Pure — all inputs explicit

function calculateTotal(price, qty, discount):
    // same inputs → always same output
    return price * qty * (1 - discount)

// Testing is trivial:
assert calculateTotal(10.0, 3, 0.10) == 27.0
assert calculateTotal(10.0, 3, 0.20) == 24.0

Side Effects and Mutations

Mutating an argument is a hidden side effect — the caller doesn't expect it. If a function receives a list and modifies it in place, every piece of code sharing that list is silently affected. Return new values instead of mutating. Isolate I/O (DB writes, HTTP calls, logging) at the boundaries of your system — keep the core logic pure.

Mutates argument — surprise!

function applyDiscount(items, discount):
    for each item in items:
        item.price = item.price * (1 - discount)  // mutates in-place!
    return items

cart = [{ name: "book", price: 20 }]
applyDiscount(cart, 0.1)
// cart is now mutated — caller doesn't know

Returns new data — no surprises

function applyDiscount(items, discount):
    result = []
    for each item in items:
        newItem = copy(item)
        newItem.price = item.price * (1 - discount)
        append newItem to result
    return result

cart = [{ name: "book", price: 20 }]
discounted = applyDiscount(cart, 0.1)
// cart unchanged, discounted is a new list

Push Side Effects to the Edge

You can't eliminate all side effects — programs must read input and produce output. The goal is to push them to the boundaries: UI layer, API handlers, repository methods. Keep the business logic layer pure. Pure core + thin impure shell is the architecture that makes codebases testable, readable, and maintainable long-term.

// ✗ Business logic entangled with I/O
function processOrder(orderId):
    order = db.find(orderId)           // I/O
    if order.total > 1000:
        order.discount = 0.05
    else:
        order.discount = 0
    order.final = order.total * (1 - order.discount)
    db.save(order)                      // I/O
    email.send(order.user, "Confirmed") // I/O
    return order.final
// ✓ Pure core — I/O isolated at the edges
function calculateDiscount(total):   // pure
    return 0.05 if total > 1000 else 0.0

function calculateFinal(total, discount):  // pure
    return total * (1 - discount)

function processOrder(orderId):   // impure shell — thin and obvious
    order    = db.find(orderId)                         // I/O
    discount = calculateDiscount(order.total)           // pure
    final    = calculateFinal(order.total, discount)    // pure
    db.saveFinal(orderId, final)                        // I/O
    email.send(order.user, "Confirmed")                 // I/O
    return final

Code Challenge

This function reads a global, mutates its argument, and has a hidden side effect. Make all inputs explicit parameters and return a value without touching anything else.

Key takeaway

Pure functions are the safest units of code — predictable, testable, and honest. Push I/O to the edges, keep the core pure.

Done with this lesson?

Mark it complete to earn XP and track your progress.