Why this matters
Functional programming's defining characteristic is the discipline of not assigning values. In a purely functional language, variables are not variable — they are named values that never change after binding. This is not a limitation; it is a guarantee.
Martin's architectural insight is that all the hardest problems in concurrent systems — race conditions, deadlocks, concurrent update bugs — arise from multiple threads mutating shared state. If no state is ever mutated, these problems cannot occur. The architecture benefits even in non-purely-functional code: functions that never modify the data they receive are safe to call from any thread, at any time, any number of times. They are the building blocks of systems that scale without fear. The discipline of immutability is not just a style preference; it is a concurrency strategy built into the architecture.
The problem
Mutating shared data in place silently destroys the original state. In concurrent scenarios, two callers modifying the same list simultaneously produce unpredictable results.
Bad
def add_tax(items: list[dict], rate: float) -> None:
for item in items:
item["price"] = item["price"] * (1 + rate) # mutates caller's list!
cart = [{"name": "Book", "price": 20.0}, {"name": "Pen", "price": 5.0}]
add_tax(cart, 0.20)
# cart is permanently modified — original prices are gone
print(cart) # [{"name": "Book", "price": 24.0}, ...]
function addTax(items: CartItem[], rate: number): void {
items.forEach(item => {
item.price = item.price * (1 + rate); // mutates original array in place
});
}
const cart = [{ name: "Book", price: 20 }, { name: "Pen", price: 5 }];
addTax(cart, 0.20);
console.log(cart); // original prices destroyed — silent data loss
The solution
A pure function returns a new list without touching the original. The caller decides what to do with the result — the function makes no decisions for them.
Good
def add_tax(items: list[dict], rate: float) -> list[dict]:
return [
{**item, "price": round(item["price"] * (1 + rate), 2)}
for item in items
]
cart = [{"name": "Book", "price": 20.0}, {"name": "Pen", "price": 5.0}]
taxed = add_tax(cart, 0.20)
print(cart) # unchanged: [{"name": "Book", "price": 20.0}, ...]
print(taxed) # new list: [{"name": "Book", "price": 24.0}, ...]
interface CartItem { name: string; price: number; }
function addTax(items: CartItem[], rate: number): CartItem[] {
return items.map(item => ({
...item,
price: Math.round(item.price * (1 + rate) * 100) / 100,
}));
}
const cart = [{ name: "Book", price: 20 }, { name: "Pen", price: 5 }];
const taxed = addTax(cart, 0.20);
console.log(cart); // unchanged: [{ name: "Book", price: 20 }, ...]
console.log(taxed); // new array: [{ name: "Book", price: 24 }, ...]
Key takeaway
If a function modifies data it didn't create, you lose the ability to reason about state — and every race condition, deadlock, and concurrent update bug in history is a consequence of that loss.