Functional Programming Concepts: Pure Functions, Immutability, and Composition
Functional programming is not just a set of language features — it is a set of constraints that, when applied, produce code that is easier to test, easier to parallelize, and easier to reason about in isolation. You can apply these ideas in Python, JavaScript, or Java without switching to Haskell.
Published June 30, 2026The core claim of functional programming is that most bugs come from hidden state changes: a function that produces different output for the same input, or a mutation that reaches further than expected. Functional style constrains functions so those surprises cannot happen.
Pure functions
A pure function has two properties: given the same input it always returns the same output, and it has no side effects. No I/O, no mutations to variables outside the function, no reading from global state.
# Pure: depends only on its arguments, changes nothing outside itself
def add(a, b):
return a + b
def multiply_all(numbers, factor):
return [n * factor for n in numbers]
# Impure: reads from external state; result changes when state changes
total = 0
def add_to_total(n):
global total
total += n # mutates external state -- side effect
return total
Pure functions are trivially testable: you call them with inputs and assert on the return value. There are no mocks, no setup, no teardown. Two pure function calls with the same arguments can be reordered or run in parallel without coordination — they cannot interfere with each other.
Immutability
Immutable data structures cannot be modified after creation. Instead of changing a list in place, you produce a new list. This guarantees that passing a value to a function cannot cause that function to alter data the caller still holds a reference to.
# Mutable approach: the original list is modified in place
def append_tax(prices, rate):
for i in range(len(prices)):
prices[i] = prices[i] * (1 + rate) # modifies the caller's list
original = [10.0, 25.0, 5.0]
append_tax(original, 0.1)
# original is now [11.0, 27.5, 5.5] -- the caller did not expect this
# Immutable approach: return a new list, leave the original intact
def with_tax(prices, rate):
return [p * (1 + rate) for p in prices]
original = [10.0, 25.0, 5.0]
taxed = with_tax(original, 0.1)
# original is still [10.0, 25.0, 5.0]
Languages like Rust enforce immutability at the compiler level: variables are immutable by default, and you must explicitly declare a variable mut to allow mutation. In JavaScript, const prevents reassignment of a binding but does not prevent mutation of the object it refers to — a common source of confusion.
Higher-order functions
A higher-order function takes another function as an argument or returns a function. This enables behavior to be parameterized without subclassing or conditional logic.
orders = [
{"id": 1, "total": 120.0, "status": "shipped"},
{"id": 2, "total": 45.0, "status": "pending"},
{"id": 3, "total": 300.0, "status": "shipped"},
{"id": 4, "total": 15.0, "status": "cancelled"},
]
shipped = list(filter(lambda o: o["status"] == "shipped", orders))
totals = list(map(lambda o: o["total"], shipped))
grand_total = sum(totals) # 420.0
# Equivalent pipeline using a list comprehension
grand_total = sum(o["total"] for o in orders if o["status"] == "shipped")
map applies a transformation to every element; filter selects elements that satisfy a predicate; reduce (in Python's functools) accumulates a sequence into a single value. These three combinators can express most data transformation pipelines without explicit loops and mutable accumulators.
Function composition
Composition combines small, focused functions into larger ones. Rather than writing a single function that does five things, you write five functions that each do one thing, then connect them.
from functools import reduce
def compose(*fns):
"""Apply functions right-to-left: compose(f, g, h)(x) == f(g(h(x)))"""
def composed(x):
return reduce(lambda v, f: f(v), reversed(fns), x)
return composed
def strip_whitespace(s): return s.strip()
def to_lowercase(s): return s.lower()
def remove_punctuation(s):
return "".join(c for c in s if c.isalnum() or c.isspace())
normalize = compose(remove_punctuation, to_lowercase, strip_whitespace)
normalize(" Hello, World! ") # "hello world"
Each step is independently testable. Adding a new normalization step means writing a new single-purpose function and inserting it into the composition — the existing steps do not change.
Closures and partial application
A closure is a function that captures variables from the scope where it was defined. Partial application fixes some arguments of a function, producing a new function that takes the remaining arguments.
from functools import partial
def power(base, exponent):
return base ** exponent
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
square(5) # 25
cube(3) # 27
# Closure: the multiplier variable is captured by the returned function
def make_multiplier(factor):
def multiply(x):
return x * factor
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
double(7) # 14
triple(7) # 21
Functional style in object-oriented codebases
You do not need a purely functional language to benefit from these ideas. The most practical application is to push impure code to the edges of the system — database reads, API calls, file I/O — and write the logic in the middle as pure functions. Business rules that transform data are easy to isolate as pure functions; they can then be tested exhaustively without any infrastructure.
When a function reads from a database to decide what to return, every unit test must set up database state. When the same logic is split into a pure transformation function and a separate data-fetching step, the transformation can be tested with plain data, and the fetch layer can be tested with a lightweight integration test. The seam between pure and impure is what makes both halves independently testable.