You are viewing a free preview of this lesson.
Subscribe to unlock all 10 lessons in this course and every other course on LearningBro.
A programming paradigm is a fundamental style of building software — a coherent set of ideas about how a program should be structured, how data and behaviour relate, and how control flows from one instruction to the next. This lesson examines the four paradigms named by H446 section 2.2: procedural, object-oriented, functional and declarative. By the end you should be able to recognise each from a code fragment, state its defining characteristics, weigh its strengths against its weaknesses, and — crucially for the higher-mark questions — justify which paradigm best fits a given scenario.
A paradigm is not the same thing as a language. A language merely supports one or more paradigms; the paradigm is the way you choose to think while writing the code. The distinction between high-level and low-level languages, and between imperative and declarative languages, is owned by the Types of Programming Language lesson in the Software Systems course — consult it for language levels and classification rather than re-deriving them here. This lesson stays focused on the paradigms themselves.
It helps to see the paradigms as answers to a recurring question: as programs grow, how do we keep them understandable? Early unstructured code used jumps (GOTO) and quickly became unreadable; procedural programming imposed discipline by grouping logic into subroutines. As systems grew larger still, even well-organised procedures struggled because data sprawled across the whole program, so object-oriented programming drew protective boundaries around state. Functional programming attacks the same complexity from a different angle — eliminate mutable state altogether so there is less to reason about — and declarative programming sidesteps the how entirely for problems where only the what matters. Seen this way, the four paradigms are not arbitrary alternatives but successive strategies for taming complexity, which is why the "best" paradigm always depends on the size and nature of the problem.
This lesson addresses the programming-paradigm strand of H446 2.2. You are expected to:
No verbatim wording from the specification is reproduced; the points above are paraphrased.
Before the four named paradigms, it helps to see the single fault line that divides them. Imperative paradigms describe how to reach a result as an explicit sequence of state-changing steps; declarative paradigms describe what result is wanted and delegate the how to a runtime, query planner or inference engine.
graph TD
P[Programming Paradigms] --> I[Imperative<br/>HOW: explicit steps]
P --> D[Declarative<br/>WHAT: describe the result]
I --> PR[Procedural]
I --> OO[Object-Oriented]
D --> FU[Functional]
D --> DC[Logic / Query / Markup]
Procedural and object-oriented programming are firmly imperative — both march through statements that mutate variables or object state. Functional programming sits awkwardly across the line: it is technically declarative in spirit (you compose expressions rather than command mutations) yet is usually grouped on its own because of how distinctive its rules are. Pure declarative languages such as SQL or Prolog sit furthest from imperative thinking. Holding this map in your head explains why the paradigms differ rather than merely that they differ — exactly the kind of reasoning the higher AO marks reward.
Procedural programming organises a program as an ordered series of instructions grouped into procedures (subroutines or functions). Execution flows top-to-bottom, steered by the three classic control structures — sequence, selection and iteration. It is the natural extension of structured programming and the style most students meet first.
if/else) and iteration (loops).# OCR-style pseudocode: a small procedural program
PROCEDURE displayMenu()
OUTPUT "1. Add item"
OUTPUT "2. Remove item"
OUTPUT "3. Quit"
END PROCEDURE
FUNCTION calculateTotal(prices: ARRAY OF REAL) RETURNS REAL
total = 0
FOR i = 0 TO LENGTH(prices) - 1
total = total + prices[i]
NEXT i
RETURN total
END FUNCTION
basket = [3.50, 1.20, 0.80]
displayMenu()
OUTPUT calculateTotal(basket) // 5.50
Trace calculateTotal([3.50, 1.20, 0.80]): total starts at 0; the loop adds 3.50 (→ 3.50), then 1.20 (→ 4.70), then 0.80 (→ 5.50); the function returns 5.50. Notice the shape of the code — do this, then this, then this — which is the procedural signature.
| Aspect | Procedural |
|---|---|
| Learning curve | Gentle — mirrors how we describe everyday processes |
| Execution flow | Easy to trace line by line |
| Best fit | Scripts, automation, batch processing, short numerical tasks |
| Data safety | Weak — shared data is exposed to every procedure; no enforced boundary |
| Scaling | Poor — large programs drift toward tangled, hard-to-follow "spaghetti" |
| Reuse | Limited — reuse is at the level of individual procedures, not whole models |
Best for: small-to-medium programs and clearly sequential problems where the overhead of objects or pure-functional discipline would not pay for itself.
The fatal weakness is the separation of data and procedures combined with shared mutable state. Consider a program that tracks a running game score in a global:
score = 0 # shared, global state
def add_points(n):
global score
score += n # any procedure may change it
def apply_penalty():
global score
score -= 5 # ...so may this one
def reset_round():
global score
score = 0 # ...and this one
Every procedure can reach in and rewrite score. In a 30-line program that is harmless; in a 30,000-line program it is a nightmare — when score ends up wrong, any procedure could be the culprit, and there is no enforced boundary to narrow the search. This is the structural reason large procedural codebases tend toward tangled "spaghetti", and it is exactly the problem encapsulation (in OOP) is designed to solve: put score inside an object and let only that object's methods touch it. Being able to articulate this cause-and-effect — shared mutable state → hard-to-trace bugs → encapsulation as the fix — is a reliable way to earn the analytical marks.
Object-oriented programming builds a program from objects — instances of classes — each bundling data (attributes) together with the behaviour (methods) that acts on that data. The program runs by objects sending messages (method calls) to one another. OOP shines when a problem maps naturally onto interacting entities with state.
| Pillar | Idea |
|---|---|
| Encapsulation | Bundle data with its methods; restrict outside access so an object guards its own state. |
| Inheritance | Derive a specialised class from a general one, reusing and extending its members. |
| Polymorphism | Let one method call behave differently depending on the object's actual type. |
| Abstraction | Expose a simple interface and hide the messy implementation behind it. |
This lesson treats the four pillars only at the level needed to characterise the paradigm. The remaining lessons in this course develop the code mechanics: OOP Fundamentals (classes, objects, constructors), Inheritance and Polymorphism, and Encapsulation and Access. The complementary skill of deciding when OOP is appropriate and modelling it with class diagrams belongs to the Object-Oriented Design lesson in the Software Systems course — cross-link to it rather than duplicating the design treatment.
The four pillars are not independent ideas bolted together; they reinforce one another. Abstraction decides what an object should expose — the simple set of operations a caller cares about — while hiding the rest. Encapsulation is the mechanism that enforces that boundary, keeping the hidden detail private. Inheritance then lets a new class accept an existing abstraction and extend it, and polymorphism lets callers use the abstraction without knowing which concrete subclass they actually hold. A useful one-line summary for the exam: abstraction is the idea of a clean interface; encapsulation is how OOP delivers it; inheritance reuses it; polymorphism varies the behaviour behind it. Quoting that chain shows the examiner you understand the pillars as a connected system rather than as four memorised words.
# OCR-style OOP pseudocode
CLASS BankAccount
PRIVATE balance: REAL
PRIVATE accountHolder: STRING
PUBLIC PROCEDURE new(holder: STRING, initialBalance: REAL)
accountHolder = holder
balance = initialBalance
END PROCEDURE
PUBLIC PROCEDURE deposit(amount: REAL)
balance = balance + amount
END PROCEDURE
PUBLIC FUNCTION getBalance() RETURNS REAL
RETURN balance
END FUNCTION
END CLASS
account = NEW BankAccount("Alice", 500.00)
account.deposit(100.00)
OUTPUT account.getBalance() // 600.00
The decisive contrast with the procedural version: balance is not a free variable any code may touch — it lives inside the object and is reachable only through deposit and getBalance. The object owns and protects its own state.
| Aspect | OOP |
|---|---|
| Modelling | Maps cleanly onto real-world entities and their relationships |
| Data safety | Strong — encapsulation prevents uncontrolled access |
| Reuse | High — inheritance and composition reuse whole behaviours |
| Maintenance | Localised — change an implementation without breaking callers |
| Overhead | Heavier — boilerplate and ceremony can swamp a tiny problem |
| Risk | Over-engineering; brittle, deep inheritance hierarchies |
Best for: large, evolving systems with many interacting entities — GUIs, games, simulations, enterprise and banking software.
Functional programming treats computation as the evaluation of mathematical functions, deliberately avoiding mutable state and side effects. A program is built by composing small functions, not by issuing a sequence of state-changing commands.
| Concept | Meaning |
|---|---|
| Pure function | Same input always yields same output; no observable side effect. |
| Immutability | Values never change after creation; transformations produce new values. |
| First-class functions | Functions are ordinary values — stored in variables, passed, returned. |
| Higher-order functions | Functions that take and/or return other functions (e.g. map, filter, reduce). |
| Recursion | Repetition expressed by self-reference, replacing imperative loops. |
| Referential transparency | An expression can be replaced by its value without changing behaviour. |
# Pure: depends only on its arguments, changes nothing outside itself
def add(a, b):
return a + b
# Impure: reads and mutates external state -> a side effect
total = 0
def add_to_total(value):
global total
total += value # side effect: 'total' is changed
add(2, 3) is 5 every time, in any context — you could replace the call with 5 and nothing breaks (referential transparency). add_to_total(3) returns nothing useful and leaves total altered, so two identical calls give different program states. Purity is what makes functional code easy to test and safe to parallelise.
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x ** 2, numbers)) # [1, 4, 9, 16, 25]
evens = list(filter(lambda x: x % 2 == 0, numbers)) # [2, 4]
from functools import reduce
total = reduce(lambda a, b: a + b, numbers) # 15
map applies its function to every element; filter keeps only those satisfying the predicate; reduce folds the list into one value (((((1+2)+3)+4)+5) = 15). None of the three mutates numbers — each returns a new result, the hallmark of immutability.
| Aspect | Functional |
|---|---|
| Testability | Excellent — pure functions have no hidden dependencies |
| Concurrency | Excellent — no shared mutable state means no data races |
| Reasoning | Predictable — referential transparency aids proof and refactoring |
| Conciseness | High — higher-order functions express transformations tersely |
| Learning curve | Steep for those trained on imperative loops and variables |
| Awkward cases | I/O and inherently stateful problems need special handling; deep recursion risks stack overflow |
Best for: data-transformation pipelines, concurrent/parallel workloads, and mathematical or scientific computation.
Because pure functional code shuns mutable loop counters, repetition is expressed by recursion — a function defined partly in terms of itself, with a base case to stop and a recursive case that moves toward it. Summing a list imperatively needs a mutable accumulator and a loop; functionally it is a self-referential definition:
def sum_list(items):
if not items: # base case: empty list sums to 0
return 0
return items[0] + sum_list(items[1:]) # recursive case
print(sum_list([4, 2, 6])) # 12
Trace sum_list([4, 2, 6]): it returns 4 + sum_list([2, 6]), which is 4 + (2 + sum_list([6])), which is 4 + (2 + (6 + sum_list([]))), and the base case makes the innermost call 0, so the whole expression collapses to 4 + 2 + 6 + 0 = 12. No variable was ever reassigned — each call simply produced a value. The cost is that every pending call occupies a frame on the call stack, so deeply recursive functional code can exhaust stack space where an imperative loop would not; this is the practical trade-off behind functional repetition and the synoptic bridge to stacks and the call stack covered later.
Declarative programming describes the what — the desired result or the logical relationships that define it — and leaves the how to the language's engine. There are no explicit loops or manual control flow; you specify constraints or a target, and the system finds a way to satisfy them.
| Language | Domain |
|---|---|
| SQL | Database queries |
| HTML | Document structure |
| CSS | Presentation/styling |
| Prolog | Logic and inference |
| Regular expressions | Pattern matching |
-- Declarative: describe the result set you want
SELECT name, score
FROM students
WHERE score > 80
ORDER BY score DESC;
Nowhere does this say how to loop over rows, compare each score, or sort the survivors — the database's query planner decides, free to use an index or a particular sort algorithm. An imperative equivalent would spell out a loop, comparisons, a results list and an explicit sort.
Logic languages push the declarative idea even further. In Prolog you state facts and rules, then pose a goal; the engine searches for combinations of facts that satisfy it, backtracking automatically when a path fails:
parent(tom, bob).
parent(bob, ann).
grandparent(X, Y) :- parent(X, Z), parent(Z, Y).
?- grandparent(tom, ann). % the engine proves this is true
You never wrote a loop or told Prolog how to chain the facts together — the rule for grandparent simply declares the logical relationship, and the inference engine works out that tom → bob → ann satisfies it. This is the purest expression of "describe what, not how": the programmer supplies knowledge, the system supplies the procedure. It also shows why declarative debugging is harder — when a query unexpectedly fails there is no step-by-step trace, only the search the engine performed internally.
| Aspect | Declarative |
|---|---|
| Conciseness | High within its domain — intent reads directly off the page |
| Optimisation | The engine can optimise execution (e.g. choose an index) |
| Focus | Programmer concentrates on the problem, not the mechanics |
| Generality | Narrow — each language is tied to a specific domain |
| Control | Little say over execution strategy |
| Debugging | Harder — there is no step-by-step trace to follow |
| Feature | Procedural | OOP | Functional | Declarative |
|---|---|---|---|---|
| Unit of organisation | Procedures | Objects | Functions | Descriptions/queries |
| Data & behaviour | Separate | Bundled (encapsulated) | Functions are primary | Implicit |
| State | Mutable variables | Mutable object state | Immutable | Engine-managed |
| Control flow | Explicit (loops, branches) | Method calls/messages | Recursion, HOFs | Implicit |
| Reuse mechanism | Subroutines | Inheritance, composition | Higher-order functions | Domain primitives |
| Imperative? | Yes | Yes | No (largely) | No |
| Typical use | Scripts, small tools | Large systems | Data pipelines, concurrency | Databases, markup |
| Scenario | Recommended paradigm | Justification |
|---|---|---|
| Short utility script | Procedural | Sequential logic; object/functional overhead unjustified |
| Large enterprise application | OOP | Encapsulation and inheritance tame complexity over time |
| Data-transformation pipeline | Functional | Pure functions compose cleanly and parallelise safely |
| Reporting over a database | Declarative (SQL) | Describe the result; let the planner optimise retrieval |
| GUI application | OOP (+ event-driven) | Widgets are stateful objects; events drive interaction |
| Numerical/scientific batch | Functional | Predictability and absence of side effects aid correctness |
A good recommendation names the paradigm and ties two or three of its defining characteristics to the scenario's demands. "Use OOP" earns little; "use OOP because the system has many interacting entities whose state must be protected, and inheritance lets shared behaviour be reused as the catalogue of account types grows" earns the marks.
Real languages rarely confine you to one paradigm; the paradigm is a choice you make per problem, not a property fixed by the language.
| Language | Paradigms supported |
|---|---|
| Python | Procedural, OOP, Functional |
| JavaScript | Procedural, OOP, Functional, Event-driven |
| Java | OOP (primary), Procedural, Functional (lambdas/streams) |
| C++ | Procedural, OOP, Functional |
| Haskell | Functional (primary) |
| SQL | Declarative |
Python is the obvious example: the very same file can run a procedural script, define classes, and pipe data through map/filter in one functional line. Knowing the paradigms lets you pick the right tool within a language rather than being limited by it.
A company is building a new system to query and report on millions of customer transactions stored in a relational database. A separate part of the project transforms raw transaction logs into cleaned, summarised datasets, with the work spread across many processor cores.
(a) Identify the most suitable paradigm for querying and reporting on the database, and justify your choice. [3]
(b) Identify the most suitable paradigm for the parallel data-transformation stage, and justify your choice. [3]
(c) A junior developer suggests writing the whole system, including the data model for customers and accounts, in a purely procedural style. Discuss the strengths and weaknesses of this suggestion compared with an object-oriented approach for the data-model part of the system. [9]
Procedural programming organises code into procedures and keeps data separate from the functions that use it. For a data model of customers and accounts this means the data would sit in variables or records that any procedure could change. This is simpler to write at first and easy to follow for a small program. Object-oriented programming instead puts the data and methods together in classes, so a
Customerobject would hold its own details and the methods to change them.The weakness of the procedural approach is that the customer data is not protected — any part of the program could set an invalid value, such as a negative balance, because there is no encapsulation. OOP fixes this by making attributes private and providing methods with validation. OOP also allows inheritance, so if there were different types of account they could share common code. Overall OOP is better for the data model because it protects the data and is easier to extend, although procedural code would be quicker to write for something very small.
The two paradigms differ most sharply in how they treat the relationship between data and behaviour, and that difference is exactly what matters for a data model. Procedural programming separates the two: customer and account data would be held in records or variables, and a set of free-standing procedures would read and update them. Object-oriented programming bundles them: a
Customerclass would own its fields and expose methods such asdepositorupdateAddress, with the fields kept private.For a small or short-lived program the procedural style has real strengths. There is less ceremony — no class definitions, constructors or access modifiers — so the code is faster to write and the top-to-bottom flow is easy to trace. If the data model were a handful of fields used by one script, procedural code would be perfectly adequate and arguably clearer.
The weaknesses appear as the model grows, and a customer/account system is precisely the kind that grows. Because procedural code leaves data exposed, nothing prevents an invalid state: any procedure, anywhere, could set a balance to a negative number or corrupt an account. Encapsulation in OOP closes this off — attributes are private and every change passes through a method that can validate it, so an account object cannot be put into an illegal state. This protection is the decisive advantage for financial data.
OOP brings two further benefits that suit this domain. Inheritance lets a general
Accountclass capture shared behaviour whileSavingsAccountandCurrentAccountspecialise it, avoiding duplicated code as the product range expands. Polymorphism then lets reporting code treat all accounts uniformly while each behaves correctly for its type. Against this, OOP carries costs: more boilerplate, a steeper learning curve, and the risk of over-engineering or rigid inheritance hierarchies if the design is rushed.On balance, for the data-model part of a system that handles money and many interacting entities and is expected to evolve, the object-oriented approach is the stronger choice: its encapsulation directly protects data integrity, and inheritance and polymorphism manage growth. The junior developer's procedural suggestion would be defensible only if the system were genuinely small and static, which a customer-and-accounts system is not. The right judgement is therefore OOP for the data model, while accepting that procedural code may still be the better tool for small scripts elsewhere in the project.
The Mid-band answer correctly contrasts the two paradigms and reaches a supported conclusion, but its evaluation is thin: it asserts that OOP "protects the data" without fully explaining the mechanism, and the procedural strengths are barely developed, so the discussion is one-sided. The Top-band answer earns the higher AO3 marks by treating the question as a genuine trade-off — it grants procedural programming real advantages for small programs, explains why encapsulation matters specifically for financial state, brings in inheritance and polymorphism with concrete account types, acknowledges OOP's costs, and only then delivers a judgement that is conditional on the system's scale and longevity. That balanced, scenario-anchored reasoning is the discriminator at the top band.
Look at this fragment and answer the parts below.
prices = [12.0, 4.5, 9.0]
result = list(filter(lambda p: p > 5, prices))
(d) State which paradigm this fragment most clearly demonstrates. [1] (e) Give one characteristic of that paradigm shown by the fragment. [2]
Indicative content: (d) the functional paradigm. (e) the use of a higher-order function (filter) that takes another function (the lambda) as an argument; equally creditworthy is that prices is not mutated — filter returns a new list, illustrating immutability. A bare "it uses a function" would not score, because procedural code also uses functions; the mark depends on naming a distinctively functional trait such as higher-order use or absence of mutation.
reduce) and reflect on which reads most clearly for that task.Exam Tip: When a question shows a code fragment and asks "which paradigm?", read for the tell-tales: free data plus top-down procedures → procedural;
class/objects/methods → OOP;map/filter/pure functions/no mutation → functional;SELECT/declarations with no control flow → declarative.
Exam Tip: For recommendation questions, structure the answer as name → two or three characteristics → explicit link to the scenario's needs. The link is where the AO2 marks live.