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.