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