Skip to content

Programming Paradigms

A programming paradigm is a way to organize state, behavior, and control flow. Most production languages are multi-paradigm. Choose the clearest local model for the problem instead of forcing an entire codebase into one ideology.

Imperative and Procedural

Imperative code states how to change program state; procedural code groups those steps into functions. It fits explicit workflows, I/O, orchestration, and algorithms whose order matters.

def transfer(source: dict, target: dict, amount: int) -> None:
    if amount <= 0 or source["balance"] < amount:
        raise ValueError("invalid transfer")
    source["balance"] -= amount
    target["balance"] += amount

Mutation is direct but requires clear ownership and atomicity. Keep state local, invariants near changes, and effects visible.

Object-Oriented

Object-oriented programming groups state with operations that preserve its invariants. It works well for domain entities with identity and lifecycle, stateful adapters, and substitutable behaviors.

Encapsulation means controlling valid access, not generating getters for every field. Polymorphism lets callers use a stable contract across meaningful implementations. Inheritance is one reuse mechanism, but it tightly couples descendants to a base class.

Prefer composition:

from dataclasses import dataclass
from typing import Protocol


class TaxPolicy(Protocol):
    def tax(self, subtotal: int) -> int: ...


@dataclass
class Order:
    subtotal: int
    tax_policy: TaxPolicy

    def total(self) -> int:
        return self.subtotal + self.tax_policy.tax(self.subtotal)

Add an interface at a real boundary or when multiple implementations exist; do not wrap every class speculatively.

Functional

Functional programming emphasizes values, expressions, pure functions, and composition. A pure function returns the same result for the same arguments and has no externally observable side effects.

from dataclasses import dataclass, replace


@dataclass(frozen=True)
class Account:
    balance: int


def debit(account: Account, amount: int) -> Account:
    if amount <= 0 or amount > account.balance:
        raise ValueError("invalid debit")
    return replace(account, balance=account.balance - amount)

Pure cores are easy to test and parallelize. Real programs still perform I/O; isolate effects at boundaries rather than pretending they do not exist. Immutability trades simpler reasoning for allocation and data-structure costs that should be measured when material.

Use comprehensions and generators when clearer than chains of map, filter, and reduce in Python.

Declarative

Declarative code describes a desired result or relation while an engine chooses execution details. SQL, regular expressions, build rules, and infrastructure definitions are examples.

Declarative systems can optimize and reconcile globally, but execution is not magic. Learn the engine's cost model, convergence, failure, and escape hatches. A concise query can still perform an expensive scan.

Event-Driven and Reactive

Event-driven systems react to discrete messages or callbacks. Reactive systems model values or streams that propagate change. Both fit user interfaces, asynchronous I/O, and continuously changing data.

Define event ownership, ordering, buffering, back-pressure, cancellation, error propagation, and subscription lifetime. Hidden callbacks and unbounded streams make control flow and resource use difficult to reason about.

Concurrent Models

Concurrency is overlapping progress; parallelism is simultaneous execution.

Shared Memory

Threads share state and coordinate with locks, atomics, or transactional mechanisms. It is efficient for tightly coupled work but risks races, deadlocks, and contention. Minimize shared mutable state and define lock ordering.

Message Passing and Actors

Tasks or actors own local state and exchange messages. This prevents direct data races across owners but not logical races, unbounded mailboxes, duplicates, or ordering mistakes.

Async/Await

Async code expresses suspension while waiting without dedicating a thread per operation. It fits high-concurrency I/O, not automatically CPU-heavy work. Cancellation, deadlines, resource limits, and blocking calls remain explicit concerns.

Data Parallelism

The same operation runs over partitions of data using vectorization, worker pools, or accelerators. It needs independent work, partition balance, bounded coordination, and deterministic reduction where required.

Logic and Rule-Based Programming

Logic programming declares facts, rules, and queries; a solver searches for consequences. Rule engines similarly separate policy facts from evaluation. They fit constraint solving and complex policies when explanation and termination are controlled.

Rules can interact non-locally. Version them, test conflicts, bound search, and make the reason for a decision observable.

Dataflow and Pipelines

Dataflow organizes computation as values moving through transformation stages:

from collections.abc import Iterable, Iterator


def positive(values: Iterable[int]) -> Iterator[int]:
    return (value for value in values if value > 0)


def squares(values: Iterable[int]) -> Iterator[int]:
    return (value * value for value in values)


result = sum(squares(positive([-2, 3, 4])))
assert result == 25

Lazy pipelines bound memory for streams, but errors may occur far from construction. Document consumption, closure, replayability, and side effects.

Metaprogramming

Decorators, macros, reflection, annotations, and code generation let programs manipulate program structure. They can remove repetitive boundary code, but also hide control flow and move errors away from source.

Prefer ordinary functions and types first. Use metaprogramming when the generated pattern is stable, inspectable, debuggable, and materially smaller.

Types

Static and dynamic typing are language properties, not paradigms. Types can encode valid states, document contracts, and support tools. Runtime validation remains necessary at untrusted boundaries.

Do not make types mirror every implementation detail. Model important invariants and let inference handle obvious local facts.

Choosing Locally

Problem Useful starting point
Ordered workflow with effects Procedural functions
Entity with identity and invariants Encapsulated object
Transformation and business calculation Pure functions and immutable values
Query or desired state Declarative expression
Many independent I/O operations Async with bounded concurrency
Independent CPU work Data parallelism or worker processes
Stateful independent agents Message passing
Stable repetitive structure Limited metaprogramming

Checklist

  • Where does mutable state live, and who owns it?
  • Are side effects visible at boundaries?
  • Is concurrency bounded with explicit cancellation and failure?
  • Does an abstraction solve current variation rather than hypothetical reuse?
  • Can a reader trace control flow and data flow?
  • Is the language's native mechanism smaller than a textbook pattern?
  • Are paradigm trade-offs measured where performance matters?

Good programs mix paradigms deliberately while keeping each module's model simple.