Programming Paradigms
Different ways to structure a program — imperative, procedural, object-oriented, functional and declarative — and what each is good for.
What a paradigm is
A programming paradigm is a style of structuring programs: a set of ideas about what a program is made of and how its parts work together. One paradigm sees a program as a list of commands that change data; another sees it as a set of functions that turn input into output; a third sees it as a description of the result you want.
A paradigm is not a language. Languages are designed with certain paradigms in mind and make those styles easy, but you can often write in several styles in one language. Knowing the paradigms helps you read unfamiliar code, recognize why a language has the features it has, and pick the clearest way to solve a problem.
The paradigms form a rough family tree. The two broad branches are imperative (describe the steps) and declarative (describe the result). Procedural and object-oriented programming are refinements of the imperative branch; functional and logic programming belong to the declarative branch.
Imperative programming
Imperative programming describes a computation as a sequence of statements, each of which changes the program's state — the current values of its variables. It mirrors how a processor works: fetch an instruction, change a value in memory or a register, move to the next instruction. Assignment, loops and conditionals are its basic tools.
temperatures ← [12, 15, 9, 18]
total ← 0
FOR EACH t IN temperatures
total ← total + t
END FOR
average ← total / LENGTH(temperatures)
OUTPUT average
To understand imperative code, you follow it line by line and keep track of how the variables change. That is easy for short pieces and gets harder as the program grows, because any statement may change any variable it can reach.
temperatures = [12, 15, 9, 18]
total = 0
for t in temperatures:
total = total + t # the state changes on every pass
average = total / len(temperatures)
print(average)
13.5
Procedural programming
Procedural programming is imperative programming organized into procedures (also called subroutines or functions): named blocks of steps that can be called with parameters and reused. The main program becomes a short sequence of calls, and each procedure solves one part of the problem. C, Pascal, structured BASIC and shell scripts are typical procedural languages.
#include <stdio.h>
/* One procedure, reused for every day's readings */
double average(const double values[], int count)
{
double total = 0.0;
for (int i = 0; i < count; i++) {
total += values[i];
}
return total / count;
}
int main(void)
{
double monday[] = {12.0, 15.0, 9.0, 18.0};
double tuesday[] = {14.0, 16.0, 13.0};
printf("Monday: %.1f\n", average(monday, 4));
printf("Tuesday: %.1f\n", average(tuesday, 3));
return 0;
}
Monday: 13.5 Tuesday: 14.3
The data (the arrays) and the procedures that work on it are kept
separate: average receives its data as parameters. The
page on
functions and abstraction
explains why splitting a program into procedures makes it easier to
understand.
Object-oriented programming
Object-oriented programming (OOP) is also imperative, but it organizes a program around objects instead of procedures. An object bundles state (its data) with behavior (the methods that work on that data), and the rest of the program talks to it only through those methods. Classes describe what a kind of object looks like; inheritance and polymorphism let related kinds of objects share code and be used interchangeably.
Where procedural code asks "which steps do I perform on this data?", object-oriented code asks "which object is responsible for this, and what message do I send it?". Java, C#, C++, Python and Ruby all support OOP. The ideas are covered in depth on Object-Oriented Concepts.
Functional programming
Functional programming builds a program by combining functions that compute new values from their inputs, rather than by changing existing values step by step. Its roots are in mathematics (the lambda calculus); Haskell, Clojure, Elixir and F# are functional languages, and most mainstream languages have borrowed its main ideas.
Pure functions and immutability
The core building block is the pure function: its result depends only on its arguments, and it has no side effects — it does not change variables outside itself, print, or write files. Closely related is immutability: instead of changing a value in place, you create a new value with the change applied. The old value stays as it was.
def add_item_in_place(cart, item):
cart.append(item) # changes the caller's list: a side effect
return cart
def add_item(cart, item):
return cart + [item] # builds a new list; the input is untouched
original = ["bread"]
updated = add_item(original, "milk")
print(original, updated)
add_item_in_place(original, "eggs")
print(original)
['bread'] ['bread', 'milk'] ['bread', 'eggs']
Why bother? A pure function can be understood, tested and reused on its own: the same input always gives the same output, and calling it cannot break anything elsewhere. Data that never changes can be shared safely, even between parts of a program that run at the same time — see Concurrency Basics. Real programs still need side effects (they must show output and save data), so functional code keeps them at the edges and the logic in the middle pure.
Functions as values
In a functional style, functions are first-class values: you can store them in variables, pass them to other functions and return them from functions, just like numbers or strings. A higher-order function is a function that takes a function as an argument or returns one.
def apply_twice(func, value): # takes a function
return func(func(value))
def make_multiplier(factor): # returns a function
def multiply(x):
return x * factor
return multiply
double = make_multiplier(2)
print(apply_twice(double, 5))
print(sorted(["pear", "fig", "banana"], key=len))
20 ['fig', 'pear', 'banana']
sorted() is a built-in higher-order function: its
key argument is a function that it calls on every item.
Short throwaway functions are often written as a
lambda, a function without a
name.
Map, filter and reduce
Three higher-order functions replace most loops over a collection:
- map applies a function to every item and gives the results;
- filter keeps only the items for which a function returns true;
- reduce (also called fold) combines all items into one value, such as a sum.
from functools import reduce
celsius = [10, 25, 30, 18]
fahrenheit = list(map(lambda c: c * 9 / 5 + 32, celsius))
warm = list(filter(lambda f: f > 70, fahrenheit))
total = reduce(lambda acc, f: acc + f, warm, 0)
print(fahrenheit)
print(warm)
print(total)
[50.0, 77.0, 86.0, 64.4] [77.0, 86.0] 163.0
JavaScript offers the same three operations as methods on arrays, which makes it natural to chain them into a pipeline:
const orders = [
{ item: "coffee", price: 3.5, paid: true },
{ item: "cake", price: 4.25, paid: false },
{ item: "tea", price: 2.75, paid: true },
];
const paidTotal = orders
.filter((order) => order.paid)
.map((order) => order.price)
.reduce((sum, price) => sum + price, 0);
console.log(paidTotal);
6.25
Declarative programming
Declarative programming describes what result you want and leaves how to compute it to the language or system. You do not write loops or decide the order of steps; an engine works that out for you.
SQL is the best-known example. This query asks for the total revenue per city for paid orders; it says nothing about which rows to read first, whether to use an index, or how to group them:
SELECT city, SUM(amount) AS revenue
FROM orders
WHERE paid = TRUE
GROUP BY city
ORDER BY revenue DESC;
The database's query planner chooses the steps, and it may choose differently tomorrow when the table is larger — without any change to the query. That is the strength of declarative code: it is short, close to the problem, and leaves room for the system to optimize.
Other declarative languages are all around you. HTML describes the
structure of a page (<h1>,
<ul>), not the steps to draw it; CSS states rules
such as h1 { font-size: 2rem; } and the browser decides
how to apply them. Regular expressions, spreadsheet formulas and
configuration languages such as those of Ansible or Kubernetes are
declarative too: you describe the desired state, and a tool makes it
so.
Event-driven and logic programming
In event-driven programming, the flow of the
program is decided by events — a click, a key press, a message from
the network, a timer. You register handlers: functions to
run when a particular event happens. An event loop waits for events
and calls the matching handlers. Graphical interfaces, web pages
(button.addEventListener("click", …)) and servers are
built this way. Inside one program this is event-driven
programming; between services the same idea is
Event-Driven Architecture, and within a class design it is the
Observer pattern.
const EventEmitter = require("node:events");
const doorbell = new EventEmitter();
// Register a handler: what to do when "ring" happens
doorbell.on("ring", (visitor) => {
console.log(`Opening the door for ${visitor}`);
});
doorbell.emit("ring", "the courier");
doorbell.emit("ring", "a neighbor");
Opening the door for the courier Opening the door for a neighbor
Logic programming, with Prolog as its best-known language, describes a problem as facts and rules, and the system answers questions by searching for values that make the rules true.
Multi-paradigm languages
Most popular languages are multi-paradigm: they
support several styles and let you mix them. Python has classes for
OOP, first-class functions, lambdas and comprehensions for a
functional style, and plain loops and assignments for imperative
code. JavaScript combines objects (based on prototypes), first-class
functions and array methods such as map, and an
event-driven model in both the browser and Node.js.
In practice you choose per problem, not per program. A rough guide:
| Paradigm | Core idea | Good for |
|---|---|---|
| Imperative / procedural | Steps that change state, grouped in procedures | Scripts, low-level and performance-critical code, simple step-by-step tasks |
| Object-oriented | Objects that bundle state and behavior | Large programs that model many kinds of things, user interfaces, games |
| Functional | Pure functions, immutable data, functions as values | Data transformations, calculations that must be easy to test, concurrent code |
| Declarative | Describe the result, not the steps | Queries (SQL), page structure and style (HTML, CSS), configuration |
| Event-driven | Handlers that react to events | User interfaces, servers, anything interactive |
The same task, three ways
Here is one small task — add up the even numbers in a list — in three styles. All three print the same result; they differ in what the reader has to follow.
Imperative
Keep a running total and change it step by step. You see every step,
and you have to trace the variable total to know the
result.
numbers = [3, 8, 5, 12, 7, 4]
total = 0
for n in numbers:
if n % 2 == 0:
total += n
print(total)
24
Functional
Combine functions: filter the even numbers, then reduce them to a sum. Nothing is changed in place; each step produces a new value.
from functools import reduce
from operator import add
numbers = [3, 8, 5, 12, 7, 4]
evens = filter(lambda n: n % 2 == 0, numbers)
print(reduce(add, evens, 0))
24
const numbers = [3, 8, 5, 12, 7, 4];
const total = numbers
.filter((n) => n % 2 === 0)
.reduce((sum, n) => sum + n, 0);
console.log(total);
24
Declarative
State the result you want: the sum of the numbers that are even. In SQL, with the numbers stored in a table:
SELECT SUM(n) FROM numbers WHERE n % 2 = 0;
Python's generator expressions come close to the same style — the code reads almost like the sentence that describes it:
numbers = [3, 8, 5, 12, 7, 4]
print(sum(n for n in numbers if n % 2 == 0))
24