A pure function's output depends on nothing but its input — but that guarantee quietly breaks the moment something ELSE can change that input's contents out from under you while you're still holding a reference to it. Immutability is functional programming's answer to this specific, recurring failure mode.
3 min read
def process_order(order):
order["status"] = "processing" # mutates the SAME dict the caller still holds
return order
my_order = {"id": 1, "status": "pending"}
result = process_order(my_order)
# my_order["status"] is now "processing" too — the caller's own variable
# changed, even though it never explicitly reassigned anythingprocess_order mutates the dictionary it was handed directly, rather than returning a new one — and because Python (like JavaScript) passes objects and dicts by reference, this silently changes the caller's own copy too, even though the caller's code never did anything that looks like a mutation. This is a real, recurring source of genuinely confusing bugs: a value changes somewhere the reader never expected, because some other, possibly distant piece of code shared a reference to the same mutable object.
def process_order(order):
return {**order, "status": "processing"} # a NEW dict, original untouched
my_order = {"id": 1, "status": "pending"}
result = process_order(my_order)
# my_order is UNCHANGED — result is a separate, new dictThe immutable version never modifies the input at all — it constructs and returns an entirely new dictionary with the desired change, leaving my_order exactly as it was. This is functional programming's actual resolution to the shared-reference problem: if nothing can ever be mutated after creation, two references to the "same" value can never diverge unexpectedly, because neither one is ever actually changed in place.
This curriculum's own Go domain covered data races directly: two goroutines reading and writing the same mutable memory concurrently, with no synchronization, produce genuinely unpredictable results. Immutable data sidesteps this entire bug class structurally — if a value can never be mutated after creation, there's nothing for concurrent access to race over; every reader sees a value that's either fully there or not yet created, never a value mid-mutation. This is precisely why several of Go's own standard idioms favor passing values rather than sharing mutable pointers across goroutines where avoidable.
big_list = [1, 2, 3, ..., 1_000_000]
new_list = big_list + [1_000_001] # naively: copies ALL million elementsA genuinely naive immutable approach — copying an entire large structure every time one element changes — is real and expensive at scale. Production functional languages and libraries solve this with persistent data structures: internally, they share the unchanged parts of the old structure with the new one (a tree-like internal representation where only the specific changed branch is actually rebuilt), so "creating a new version" costs roughly proportional to what actually changed, not the size of the entire structure. React's own reconciliation model (covered in this curriculum's React domain) relies on exactly this idea at the application level — comparing old and new state efficiently depends on state actually being treated as immutable between renders.
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why can mutating a dict passed into a function surprise the function's caller?
2. Why does immutability structurally prevent data races in concurrent code?
3. How do persistent data structures make immutability practical at scale?
Functional Programming Concepts