New: Try Voli The Bear, Fast package manager (and not only) for Windows
Updated Jul 10, 2026 Edit on GitHub

What Functional Programming Actually Is

Functional programming has a reputation for being the hard, mathematical one - monads, lambda calculus, people who say "referential transparency" at parties. That reputation scares off working developers who'd actually love most of what FP offers.

Set the jargon aside. At its heart, FP is a few down-to-earth habits about where your logic lives and how you treat your data. Adopt them in any language with functions, and they pay off immediately in code that's easier to test and trust.

The core idea: functions are the main building block

What it actually is. Where OOP organizes a program around objects that hold data, functional programming organizes it around functions that take data in and return new data out. Data and behavior stay separate: your data is plain values, and your functions transform those values. A function is a first-class citizen - you can pass it to another function, return it from one, and store it in a variable, exactly like a number or a string.

That's the shift in worldview. In OOP you ask "what objects do I have, and what can they do?" In FP you ask "what transformations turn my input into my output?"

Immutability - don't change data, return new data

The problem it solves. When data can be changed in place ("mutated"), tracking who changed what, when becomes a whole category of bugs. You pass a list to a function, the function quietly modifies it, and code somewhere else is now holding a list that changed under its feet. These bugs are maddening because the broken value looks fine where you're reading it - the damage happened elsewhere.

What it actually is. Immutability means you don't modify existing data; you produce a new value with the change applied, and leave the original alone.

📝 Mutation - changing a value in place (list.append(x) changes the existing list). Immutability - instead of changing the original, you build and return a new value, so the original is untouched.

A real example. Same task - add an item to a cart - done both ways:

# Mutating: changes the original list in place
def add_item_mutating(cart, item):
    cart.append(item)
    return cart

# Immutable: leaves the original alone, returns a new list
def add_item_immutable(cart, item):
    return cart + [item]

original = ["apple"]
new_cart = add_item_immutable(original, "banana")
print(original)   # untouched
print(new_cart)   # the new version
$ python cart.py
['apple']
['apple', 'banana']

What just happened: The immutable version built a brand-new list (cart + [item] creates a new list rather than modifying cart) and returned it. original is exactly as it was - nobody holding a reference to it gets surprised. The mutating version, by contrast, would have changed original for everyone pointing at it.

Why this saves you later. When data can't change out from under you, "how did this value get like this?" stops being a mystery. Hand the same list to ten functions and know none of them altered it. Immutability is also what makes a lot of safe concurrency possible - more on that in Phase 3.

Pure functions - same input, same output, no surprises

What it actually is. A pure function has two properties: (1) given the same inputs it always returns the same output, and (2) it has no side effects - it doesn't change anything outside itself (no writing to a global, no printing, no touching a file or database, no mutating its arguments).

📝 Side effect - anything a function does beyond computing and returning a value: printing, writing to disk, sending a network request, modifying a global or its inputs. Useful and necessary - but harder to reason about.

A real example. Two functions that both "add tax":

total = 0   # a global the impure version secretly depends on

# Impure: reads/writes a global, so its result depends on hidden state
def add_tax_impure(price):
    global total
    total += price * 0.2
    return total

# Pure: depends only on its input, changes nothing outside
def add_tax_pure(price):
    return price * 0.2
>>> add_tax_pure(100)
20.0
>>> add_tax_pure(100)
20.0
>>> add_tax_impure(100)
20.0
>>> add_tax_impure(100)
40.0

What just happened: add_tax_pure(100) returns 20.0 every single time, forever - it depends only on what you pass in. add_tax_impure(100) returns a different number on the second call because it secretly reads and updates the total global. The pure one you understand by reading it alone; the impure one you can only understand by also knowing the program's entire history.

Why this saves you later - especially for testing. A pure function is the easiest thing in the world to test: pass inputs, assert on the output, done. No database to spin up, no mocks, no "set up this global first," no cleanup. This is the single most practical reason working developers adopt FP habits even in OOP languages: logic written as pure functions is logic you can test in two lines.

⚠️ Gotcha - you can't make everything pure, and shouldn't try. A program that never printed, saved, or sent anything would be useless; side effects are the point of software. The functional move is to push them to the edges - a large core of pure functions doing the real logic, and a thin outer shell doing the I/O. Then almost all your code is the easy-to-test kind, and the risky part is small and isolated.

Composition - build big behavior from small functions

What it actually is. Function composition means writing small, single-purpose functions and chaining them so the output of one feeds the next. Instead of one big function that does five things, you write five small functions and connect them.

A real example. Take a list of order amounts, drop the refunds (negatives), apply tax, and sum the total:

orders = [100, -20, 50, 30]

taxed_total = sum(
    amount * 1.2
    for amount in orders
    if amount > 0
)
print(taxed_total)
$ python compose.py
216.0

What just happened: This expresses the work as a pipeline of small transformations - keep the positives, apply tax to each, add them up - rather than a loop with a running variable we mutate. (100 + 50 + 30 = 180, times 1.2, is 216.0.) Read it top to bottom and the intent is right there: filter, transform, reduce.

Why this saves you later. Small composed functions are small things to understand, test, and reuse. When the requirement changes to "also exclude orders over 1000," you add one condition to the filter step instead of untangling a big imperative loop. And because each piece is pure, you can test the filter and the tax separately and trust the whole.

Recap

  1. Functions are the core unit - data and behavior stay separate; functions take values in and return values out.
  2. Immutability - don't change data in place; return new values, so nothing changes under anyone's feet.
  3. Pure functions - same input, same output, no side effects - which makes them trivial to reason about and to test.
  4. Push side effects to the edges - a big pure core, a thin I/O shell.
  5. Composition - build big behavior by chaining small, single-purpose functions.

That's FP's worldview: model your program as data flowing through transformations. Now you've got both mental models - so we can finally have the plain conversation about which to reach for, and when.


← Phase 1: What OOP Actually Is · Guide overview · Phase 3: Plainly - Which, When? →

Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.

Check your understanding 2 questions

1. A pure function...

2. Immutability means...