You are viewing a free preview of this lesson.
Subscribe to unlock all 10 lessons in this course and every other course on LearningBro.
This lesson surveys the major programming paradigms — the fundamentally different styles in which programs can be structured and written. A paradigm is more than a syntax choice; it is a way of thinking about computation, with its own model of data, state and control flow. Understanding paradigms lets you recognise the right tool for a problem, read code written in unfamiliar styles, and explain why most modern languages deliberately support several paradigms at once. This overview also anchors the A-Level treatment of language classification and the deeper study of functional programming.
This lesson addresses programming paradigms within the Fundamentals of programming and Theory of computation sections of the AQA A-Level Computer Science (7517) specification — drawing the comparison expected around subject content area 4.1.2 and the classification of programming languages in 4.6.2, with a forward link to functional programming in 4.12. It covers the procedural, object-oriented, functional and declarative (including logic) paradigms (with event-driven programming as a structural style), their core characteristics, their advantages and disadvantages, and — the examined skill — when each paradigm is appropriate and why a particular one suits a given problem.
A programming paradigm is a fundamental approach to programming that shapes how a programmer models a problem and structures a solution. Two paradigms can solve the same problem with utterly different code: one might describe an explicit sequence of steps, another a set of objects exchanging messages, a third a collection of mathematical functions, and a fourth merely the result desired, leaving the "how" to the system.
A useful first division is between imperative paradigms, which describe how to compute a result through explicit, ordered, state-changing instructions, and declarative paradigms, which describe what result is wanted and leave the strategy to the language.
flowchart TB
P["Programming paradigms"] --> IMP["Imperative
(how to do it)"]
P --> DEC["Declarative
(what is wanted)"]
IMP --> PROC["Procedural"]
IMP --> OOP["Object-oriented"]
DEC --> FUNC["Functional"]
DEC --> LOGIC["Logic / declarative
(Prolog, SQL)"]
| Paradigm | Core idea | Imperative or declarative |
|---|---|---|
| Procedural | Sequences of instructions grouped into procedures | Imperative |
| Object-oriented | Objects bundling data with the methods that act on it | Imperative |
| Functional | Evaluation of pure mathematical functions, no side effects | Declarative |
| Logic / declarative | State facts and rules; the engine derives answers | Declarative |
| Event-driven | Code reacts to events via handlers | A structural style (usually imperative) |
Procedural programming structures a program as a sequence of instructions grouped into procedures (subroutines/functions) that operate on data passed to them. It is the classic imperative style built from sequence, selection and iteration plus procedure calls.
# AQA-style pseudocode
PROCEDURE greet(name: STRING)
OUTPUT "Hello, " + name
ENDPROCEDURE
FUNCTION add(a: INTEGER, b: INTEGER) RETURNS INTEGER
RETURN a + b
ENDFUNCTION
greet("Alice")
result = add(3, 5)
OUTPUT result
def greet(name: str) -> None:
print(f"Hello, {name}")
def add(a: int, b: int) -> int:
return a + b
greet("Alice")
print(add(3, 5))
The defining characteristic is that data and the procedures that act on it are separate — any procedure may read or change any data it can reach. This is simple and intuitive for small programs and scripts, and the flow of execution is easy to follow. The weaknesses appear at scale: with no encapsulation, large procedural programs become hard to reason about (the proverbial "spaghetti code"), data integrity is easily compromised because nothing protects the data, and reuse is limited compared with object-oriented or functional styles.
Covered in depth earlier in this course, OOP organises a program around objects — instances of classes that bundle data (attributes) together with the behaviour (methods) that operates on that data. Its four pillars are:
| Principle | Meaning |
|---|---|
| Encapsulation | Data and methods are bundled; access to internal state is controlled. |
| Inheritance | A class can derive attributes and methods from a parent class. |
| Polymorphism | Different classes respond to the same method call in their own way. |
| Abstraction | Complex internals are hidden behind a simple public interface. |
By contrast with procedural code, OOP binds data to the code that is allowed to change it, which protects integrity and lets large systems be decomposed into self-contained, reusable objects that model real-world entities. The costs are greater complexity, a steeper learning curve, and a temptation to over-engineer simple problems for which a short procedural script would suffice.
Functional programming treats computation as the evaluation of mathematical functions, deliberately avoiding mutable state and side effects. Because a function's result depends only on its arguments, programs become far easier to reason about, test and parallelise.
| Concept | Meaning |
|---|---|
| Pure function | Same input always yields the same output; no side effects. |
| Immutability | Data is never modified in place; new values are produced instead. |
| First-class functions | Functions can be stored in variables, passed as arguments and returned. |
| Higher-order functions | Functions that take or return other functions. |
| Recursion | Repetition is expressed by recursion rather than mutable loop counters. |
| No side effects | No reliance on or modification of external/global state. |
# Pure: depends only on its arguments, changes nothing outside itself
def add(a: int, b: int) -> int:
return a + b
# Impure: a SIDE EFFECT — it mutates external state
total = 0
def add_to_total(value: int) -> None:
global total
total += value # changing a global makes this impure
numbers = [1, 2, 3, 4, 5]
# map: apply a function to every element
squared = list(map(lambda x: x ** 2, numbers)) # [1, 4, 9, 16, 25]
# filter: keep elements satisfying a predicate
evens = list(filter(lambda x: x % 2 == 0, numbers)) # [2, 4]
# reduce: fold the elements into a single value
from functools import reduce
total = reduce(lambda a, b: a + b, numbers) # 15
The benefits are substantial: pure functions have no hidden dependencies, so they are trivial to unit-test; the absence of shared mutable state makes functional code naturally safe to parallelise; and map/filter/reduce express common data transformations concisely. The trade-offs are a steeper learning curve for those used to imperative loops, possible inefficiency where a problem is naturally stateful, and the need for special techniques to manage the I/O and state that real programs inevitably require. Haskell is purely functional; Python, JavaScript, Scala and F# support functional style within a multi-paradigm setting. (Functional programming is studied in much greater depth later in the course, §4.12.)
Declarative programming describes what result is wanted, not the step-by-step procedure to obtain it; the language's engine decides how. The programmer states relationships, constraints or queries and leaves the execution strategy to the runtime.
Logic programming (such as Prolog) is a prominent declarative sub-style: the programmer asserts facts and rules, then poses a query, and an inference engine searches for all values that satisfy it.
% Facts
parent(tom, bob).
parent(bob, ann).
% Rule: X is a grandparent of Y if X is a parent of Z and Z is a parent of Y
grandparent(X, Y) :- parent(X, Z), parent(Z, Y).
% Query: grandparent(tom, ann). -> true
The programmer never writes a loop or a search algorithm; the engine performs the search by unification and backtracking. SQL is the declarative language students meet most often:
-- Describe WHAT is wanted; the DBMS decides how to retrieve it
SELECT name, score
FROM students
WHERE score > 80
ORDER BY score DESC;
The equivalent procedural solution would need explicit loops, comparisons and a sort. Declarative code is therefore often far more concise and lets the runtime optimise execution (a database query planner may choose indexes and join orders automatically). The costs are reduced control over how the computation runs, applicability that is usually confined to a specific domain (SQL for data, Prolog for logic, HTML/CSS for documents), and debugging that can be harder because the control flow is implicit.
| Declarative language | Domain |
|---|---|
| SQL | Database queries |
| Prolog | Logic programming |
| HTML / CSS | Document structure and styling |
| Regular expressions | Text pattern matching |
Event-driven programming structures a program around events — occurrences such as a mouse click, key press, timer tick or arriving message — and the handlers that respond to them. It dominates GUIs, web front-ends and games.
| Concept | Meaning |
|---|---|
| Event | A detectable occurrence (click, keypress, timer, network message). |
| Event handler | A function called when its associated event fires. |
| Event loop | A loop that waits for events and dispatches each to its handler. |
| Callback | A function passed in to be invoked later, when an event occurs. |
def on_button_click():
print("Button was clicked!")
def on_key_press(key):
print(f"Key pressed: {key}")
# A GUI framework would register the handlers, e.g.:
# button.on_click(on_button_click)
# window.on_key_press(on_key_press)
Rather than running top to bottom, an event-driven program spends most of its life waiting in the event loop, springing into action only when something happens — the natural model for interactive software. Event-driven programming is a structural style that is usually layered on top of an imperative paradigm rather than an alternative to procedural or OOP.
| Feature | Procedural | OOP | Functional | Declarative |
|---|---|---|---|---|
| Data and code | Separate | Bundled in objects | Functions are primary | Implicit / relational |
| State | Mutable variables | Mutable object attributes | Immutable | Varies |
| Control flow | Explicit (loops, branches) | Method calls between objects | Recursion, higher-order functions | Implicit (engine decides) |
| Reuse mechanism | Procedures | Inheritance and composition | Higher-order functions | Domain abstractions |
| Ease of parallelising | Hard (shared state) | Hard (shared state) | Easy (no shared mutable state) | Often automatic |
| Best suited to | Scripts, small utilities | Large interactive systems, GUIs | Data transformation, concurrency | Databases, querying, markup |
Most real languages are multi-paradigm, letting the programmer pick the best style per problem — a key idea for the classification of programming languages (§4.6.2).
| Language | Paradigms supported |
|---|---|
| Python | Procedural, OOP, functional |
| JavaScript | Procedural, OOP, functional, event-driven |
| Java | OOP (primary), procedural, functional (since Java 8) |
| C++ | Procedural, OOP, functional |
| Haskell | Functional (primary) |
| Prolog | Logic / declarative |
| SQL | Declarative |
Classification also distinguishes low-level languages (machine code and assembly, tied to a specific processor's instruction set) from high-level languages (abstracted from hardware, portable, closer to human concepts). The paradigms above are properties of high-level languages; the imperative/declarative split and the procedural/OOP/functional/logic families are exactly the vocabulary used to classify a language by its dominant style.
Exam Tip: Two question types recur: (1) "identify the paradigm" of a given code snippet — look for the giveaway features (classes ⇒ OOP, pure functions and
map/filter⇒ functional, facts-and-rules ⇒ logic,SELECT⇒ declarative); and (2) "which paradigm suits this scenario and why" — always justify against the problem's characteristics (scale, statefulness, concurrency, domain), not by reciting generic advantages.
Nothing illustrates the differences between paradigms more sharply than solving the same problem four ways. The task: given a list of numbers, compute the sum of the squares of the even numbers. The answer is identical; the way of thinking is completely different.
The procedural solution states how, step by step, using an explicit loop and a mutable accumulator.
def sum_even_squares(numbers):
total = 0 # mutable state
for n in numbers: # explicit iteration
if n % 2 == 0: # explicit selection
total = total + n * n # mutate the accumulator
return total
The OOP solution bundles the data with the operation inside an object; the calculation becomes a method that the object performs on its own state.
class NumberSet:
def __init__(self, numbers):
self._numbers = numbers # encapsulated data
def sum_even_squares(self): # behaviour bound to the data
total = 0
for n in self._numbers:
if n % 2 == 0:
total += n * n
return total
result = NumberSet([1, 2, 3, 4]).sum_even_squares() # 20
The functional solution describes the computation as a pipeline of pure transformations — filter the evens, map each to its square, reduce by summing — with no mutable variable and no loop.
from functools import reduce
def sum_even_squares(numbers):
return reduce(
lambda acc, n: acc + n,
map(lambda n: n * n,
filter(lambda n: n % 2 == 0, numbers)),
0,
)
In a declarative database query, you state what set of rows you want and what aggregate to compute; the engine decides how.
SELECT SUM(value * value)
FROM numbers
WHERE value % 2 = 0;
| Paradigm | How the "evens" filter is expressed | How repetition happens | Mutable state? |
|---|---|---|---|
| Procedural | An if inside a loop | Explicit for loop | Yes (total) |
| OOP | An if inside a method's loop | Explicit loop, inside an object | Yes (total) |
| Functional | A filter with a predicate | Implicit, via map/reduce | No |
| Declarative | A WHERE clause | Implicit, by the engine | No |
Subscribe to continue reading
Get full access to this lesson and all 10 lessons in this course.