# Object-Oriented vs Functional, Plainly

> Two ways of organizing code, demystified: what object-oriented programming actually is, what functional programming actually is, and the plain truth about which to reach for and when.


---

# Object-Oriented vs Functional, Plainly

You've seen the arguments. One person swears object-oriented programming is bloated ceremony; another says functional programming is academic and unreadable. Both sound confident, and you're left wondering whether you picked the "wrong" side by accident - or whether you even picked a side at all.

Here's the calm version of the truth: **these are two ways of organizing code, not two warring religions.** Object-oriented programming (OOP) bundles data together with the behavior that acts on it. Functional programming (FP) treats functions as the main building block and avoids changing data in place. Each solves a real problem. Most languages you already use - Python, JavaScript, C#, Scala - let you do both, and most real codebases mix them.

This guide gives you the working mental model for each, with small annotated examples, then a clear-eyed comparison of where each one actually shines. No dogma, no winner declared. By the end you'll read code in either style and make a deliberate choice instead of an inherited one.

## How to read this

- **Want the straight "which one should I use" answer?** Skip to [Phase 3: Plainly - Which, When?](03-which-when.md). It has the comparison table and the judgment calls, flagged as judgment.
- **Want it to finally make sense?** Read in order. Phase 1 and Phase 2 build the two mental models you'll need before the comparison in Phase 3 means anything.

## The phases

1. **[What OOP Actually Is](01-what-oop-actually-is.md)** - bundling data with behavior (objects and classes), and the three ideas people always cite - encapsulation, inheritance, polymorphism - explained by the problem each one solves.
2. **[What Functional Programming Actually Is](02-what-functional-actually-is.md)** - functions as the core unit, immutability, pure functions, and composing small functions into bigger ones, and why that makes code easier to test and reason about.
3. **[Plainly: Which, When?](03-which-when.md)** - the plain truth that most real code is both, a fair comparison of where each shines, and how to choose without joining a cult.

> This guide is about the two *paradigms* and the mental models behind them. It is not a tutorial in any one language's class syntax or a deep dive into category theory - for the broader "how languages differ" picture, see [Languages Explained Like a Human](/guides/languages-explained-like-a-human).


---

# What OOP Actually Is

If you learned to program in the last twenty years, OOP was probably the water you swam in - `class` this, `self` that - without anyone explaining the *point*. You wrote classes the way you'd been shown, half-suspecting it was ceremony.

It isn't. There's one core idea underneath all of it, and once you see it, the three big buzzwords (encapsulation, inheritance, polymorphism) stop being vocabulary and become tools that each fix a specific, nameable problem.

## The core idea: bundle data with the behavior that acts on it

**What it actually is.** Object-oriented programming groups together some *data* and the *functions that operate on that data* into one unit. That unit is an **object**. The blueprint for making objects of a certain kind is a **class**.

📝 **Object** - a bundle of related data plus the operations that belong with it. **Class** - the template that says what data and operations every object of that type has. You write the class once; you can make many objects from it.

**Why this exists.** Picture a bank account in a program with no objects. You'd have a loose `balance` number floating around, and separate functions `deposit(balance, amount)` and `withdraw(balance, amount)` somewhere else entirely. Nothing stops some unrelated code from setting `balance = -9999` directly. The data and the rules that protect it live in different places, and keeping them in sync is on you.

OOP's answer: put the balance and the only functions allowed to change it in the same box, and make the box guard its own data.

**A real example.** Here's that bank account as a class. The comments are the whole point - read them.

```python runnable
class BankAccount:
    def __init__(self, owner):
        self.owner = owner        # data that belongs to this account
        self._balance = 0         # the leading _ means "internal, don't touch from outside"

    def deposit(self, amount):    # behavior that acts on the data
        if amount <= 0:
            raise ValueError("deposit must be positive")
        self._balance += amount

    def withdraw(self, amount):
        if amount > self._balance:
            raise ValueError("insufficient funds")
        self._balance -= amount

    def balance(self):
        return self._balance

account = BankAccount("Nika")
account.deposit(100)
account.withdraw(30)
print(account.balance())
```
```console
$ python bank.py
70
```
*What just happened:* The object carries its own `_balance` around with it, and every change has to go through `deposit` or `withdraw`, which enforce the rules (no negative deposits, no overdrawing). Data and its guardrails travel together as one thing.

## Encapsulation - hide the internals, expose a safe surface

**The problem it solves.** If any code anywhere can reach in and change an object's data directly, then "what could possibly modify this balance?" has the answer "literally anything," and that's a debugging nightmare.

**What it actually is.** Encapsulation means keeping an object's internal data private and only letting the outside world touch it through methods you chose to expose. In the example above, `_balance` is hidden; the only doors in are `deposit`, `withdraw`, and `balance`.

**Why this saves you later.** When a balance goes wrong, you have a short list of suspects - the handful of methods that can change it - instead of the entire codebase. The object becomes something you can *trust*, because it can't be put into a nonsensical state from outside.

## Inheritance - share behavior between related types

**The problem it solves.** Sometimes you have several kinds of thing that are mostly the same with small differences. A `SavingsAccount` is a `BankAccount` that also earns interest. Copy-pasting all the account code and adding one method is how you end up with five slightly-different copies that drift apart.

**What it actually is.** Inheritance lets one class build on another. The child class gets everything the parent has, and adds or changes only what's different.

```python
class SavingsAccount(BankAccount):
    def __init__(self, owner, rate):
        super().__init__(owner)   # reuse the parent's setup
        self.rate = rate

    def add_interest(self):
        self.deposit(self._balance * self.rate)   # reuse deposit's rules

savings = SavingsAccount("Nika", 0.05)
savings.deposit(1000)
savings.add_interest()
print(savings.balance())
```
```console
$ python savings.py
1050.0
```
*What just happened:* `SavingsAccount` didn't redefine `deposit`, `withdraw`, or `balance` - it inherited them and added one new method. It even calls the inherited `deposit` inside `add_interest`, so "no negative amounts" still applies. We described one difference, not a whole new account.

⚠️ **The classic trap: inheritance overuse.** Inheritance is the OOP feature people reach for too often, and it bites hard. The moment a `Penguin` inherits from `Bird` and you discover `Bird` has a `fly()` method, you've built a lie into your type system. Deep inheritance chains (class extends class extends class, four levels down) become impossible to reason about, because understanding one object means mentally merging four files. The widely-repeated guideline is **"favor composition over inheritance"** - instead of saying a `Car` *is a* `Engine`, give the `Car` an engine as one of its pieces (`self.engine = Engine()`). Use inheritance only for genuine "is-a, and always will be" relationships, and keep chains shallow. When in doubt, hold an object as a field rather than inheriting from it.

## Polymorphism - one interface, many behaviors

**The problem it solves.** You have a list of different objects and want to do "the same thing" to each - but what that thing *means* differs per object. Without polymorphism you write a giant `if type == "savings": ... elif type == "checking": ...` that grows every time you add a type.

**What it actually is.** Polymorphism (Greek for "many shapes") means different object types can respond to the same method call in their own way, and the calling code doesn't need to know which type it's holding.

```python
def describe(account):
    print(f"{account.owner}: {account.balance()}")   # works for ANY account type

accounts = [BankAccount("Ana"), SavingsAccount("Bo", 0.05)]
for acc in accounts:
    acc.deposit(50)
    describe(acc)
```
```console
$ python poly.py
Ana: 50
Bo: 50
```
*What just happened:* `describe` and the loop call `deposit` and `balance` without caring whether `acc` is a plain account or a savings account. Add a new account type tomorrow and this code keeps working untouched - that's the payoff.

**Why this saves you later.** Polymorphism is how you add new cases without editing old code. Every `if/elif` chain on a type field needs editing whenever a type is added; polymorphism turns that into "just write the new class." Fewer edits to working code means fewer chances to break it.

## Recap

1. **The core idea** of OOP is bundling data together with the behavior that acts on it, into objects (made from classes).
2. **Encapsulation** hides an object's internals so it can't be put in a bad state from outside - fewer suspects when something goes wrong.
3. **Inheritance** shares behavior between genuinely related types - but overusing it (deep chains, fake "is-a" relationships) is the classic OOP trap. Favor composition.
4. **Polymorphism** lets many types answer the same call their own way, so you add new types instead of editing old conditionals.

That's OOP's worldview: model your program as cooperating objects, each guarding its own data. Next we'll look at the other major worldview - one that starts not from objects, but from functions.


---

# 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:

```python runnable
# 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
```
```console
$ 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](03-which-when.md).

## 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":

```python
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
```
```console
>>> 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:

```python runnable
orders = [100, -20, 50, 30]

taxed_total = sum(
    amount * 1.2
    for amount in orders
    if amount > 0
)
print(taxed_total)
```
```console
$ 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.


---

# Plainly: Which, When?

Now the question you actually came for. You understand both worldviews - so which one is *right*?

The most important sentence in this guide: **you don't have to choose, and the most common languages don't make you.** Python, JavaScript, C#, Java (modern versions), Ruby, Scala, Kotlin, Rust - all let you write objects *and* pure functions, and good codebases written in them use both, deliberately, for different jobs. OOP and FP are tools in one toolbox, not teams you join.

## "But which one is my language?"

Most languages people argue about are **multi-paradigm** - they support more than one style and let you pick per situation.

```mermaid
flowchart LR
  OO["strongly OO<br/>Java (older)*"] --> Mixed["mixed / multi-paradigm<br/>Python · JavaScript · C#<br/>Scala · Kotlin · Rust"] --> FP["strongly functional<br/>Haskell · Clojure · Elm"]
```

*\* Even Java has added lambdas, streams, and records - it has drifted toward the middle.*

The takeaway: if you write Python or JavaScript, you're *already* free to use objects where they help and pure functions where they help. "Which language is functional" matters far less than "which approach fits this particular piece of code."

## Where each one genuinely shines

Each paradigm is strong exactly where its core idea pays off.

**OOP shines when you're modeling stateful entities with identity.** A user, a shopping cart, a game character, a database connection, an open file - things that *have* state, that change over time, that you talk about as "a thing." Bundling that state with the operations that guard it (encapsulation) is a genuinely good fit. OOP also gives a large team a shared, nameable structure: "the `OrderService` owns order logic" is an organizing principle that scales across many people and files.

**FP shines when you're transforming data.** Take input, run it through steps, produce output: parsing, report generation, ETL pipelines, anything analytics-shaped. It also shines for **concurrency** - when nothing can be mutated, multiple threads can read the same data with no locks and no race conditions, because there's nothing to race over. And it shines for **testability**: pure functions need no setup and no mocks, so the test is just "input → expected output."

## The plain comparison table

This covers both sides fairly - including each one's costs.

| Dimension | Object-Oriented | Functional |
|---|---|---|
| **Core unit** | Objects (data + behavior bundled) | Functions (data and behavior kept separate) |
| **How it handles state** | Embraces it - objects hold and guard mutable state | Avoids it - prefers immutable values, isolates change |
| **Best fit** | Modeling stateful entities with identity (user, cart, connection) | Transforming data (pipelines, parsing, analytics) |
| **Concurrency** | Needs care - shared mutable state means locks and races | Strong - immutable data is safe to share across threads |
| **Testability** | Often needs setup/mocks for objects with dependencies | Pure functions test with plain input → output |
| **Team scaling** | Familiar structure; classes give large teams clear ownership | Small composable functions; fewer shared conventions in practice |
| **Common failure mode** | Deep inheritance, over-modeling, tangled mutable state | Over-abstraction, jargon, awkward I/O when taken to extremes |
| **The thing it makes easy** | "This thing exists and protects its own rules" | "This input reliably produces this output" |

📝 A note on reading this table: no row crowns a winner. Each paradigm's strength has a matching cost. OOP's comfort with state is exactly what makes its concurrency story harder. FP's purity is exactly what can make plain old "write to the database" feel awkward if you go too far. That symmetry is the whole point.

## How real codebases actually mix them

In practice, a healthy codebase doesn't pick one. A common, pragmatic pattern looks like this:

- **Objects at the boundaries and for stateful things** - the `User`, the `HttpServer`, the database connection, the cart. Things with identity and lifecycle.
- **Pure functions for the logic in the middle** - pricing rules, validation, formatting, transformations. The stuff you want to test in two lines.

A `Cart` object (OOP) might hold the items, while the function that computes the discounted total (FP, pure) takes the items and returns a number without touching anything. You get encapsulation where you want a guarded thing, and testable purity where you want trustworthy logic. That blend is not a compromise - it's most experienced developers' default.

## The judgment part (flagged as judgment)

Everything above this line is reasonably uncontroversial. What follows is *opinion* - useful heuristics, not laws:

- *In my experience,* reaching for a pure function first and only introducing an object when something genuinely needs to hold state leads to code that's easier to test. But that's a lean, not a rule.
- *I'd treat* any inheritance chain deeper than one or two levels as a smell worth a second look (this echoes the inheritance trap from [Phase 1](01-what-oop-actually-is.md)). Others disagree and use deeper hierarchies happily.
- *I'd be wary* of anyone who tells you one paradigm is correct and the other is obsolete. That's usually a sign they've worked in one world and not the other.

Take these as a senior colleague's leanings, weigh them against your own context, and discard the ones that don't fit your team.

## Recap

1. **You usually don't have to choose** - the common languages are multi-paradigm, and good codebases use both.
2. **OOP shines** at modeling stateful entities with identity and at giving large teams a clear structure.
3. **FP shines** at data transformation, at concurrency (immutable data is safe to share), and at testability (pure functions need no setup).
4. **Real codebases mix them** - objects for stateful boundaries, pure functions for the logic in between.
5. **They're tools, not religions.** Match the approach to the job, flag your opinions as opinions, and distrust anyone selling a one-true-way.

You can now read code in either style, name what it's doing and why, and make a deliberate choice instead of an inherited one - which was the whole goal.

## Where to go next

- [Languages Explained Like a Human](/guides/languages-explained-like-a-human) - how programming languages differ beyond paradigm, in plain terms.
- [Data Structures Explained](/guides/data-structures-explained) - the values your functions transform and your objects hold, demystified.
