You are viewing a free preview of this lesson.
Subscribe to unlock all 10 lessons in this course and every other course on LearningBro.
Every program — however large or sophisticated — is assembled from a small set of fundamental programming techniques. This lesson works through them in the order they build on one another: the three control-flow constructs (sequence, selection, iteration); the distinction between variables and constants; subroutines split into procedures and functions; how data is handed to a subroutine through parameters (by value and by reference); the scope rules that decide where a name is visible (local versus global); and finally an introduction to recursion, where a subroutine calls itself. These are the constructs you will reach for in every piece of code you write at A-Level, so fluency with them — including the ability to trace them by hand — underpins almost every exam question in section 2.2.
The examples below appear in both OCR-style pseudocode and Python so you can move comfortably between the two. The companion lessons on object-oriented programming build on top of these techniques; here we stay with the universal building blocks that apply in any paradigm. A recurring theme is tracing — following a fragment line by line, keeping track of each variable's value — because so many H446 questions ask you to predict the output, complete a trace table, or find and correct a bug, and every one of those tasks is really a test of how carefully and systematically you can trace these constructs by hand.
Within H446 2.2, this lesson covers the core programming constructs and structured-programming ideas. You should be able to:
if/switch) and iteration (count- and condition-controlled loops);These points paraphrase the specification; nothing is quoted verbatim.
Sequence is the most basic construct: statements execute one after another, top to bottom, in the order written. It is so fundamental it is easy to overlook, but order matters — swap two lines and the program may break.
# Sequence: each statement runs in order; order is significant
name = input("Enter your name: ")
age = int(input("Enter your age: ")) # 'age' must exist...
print(f"Hello {name}, you are {age} years old") # ...before it is used here
If the print came before the input lines, name and age would not yet exist and the program would fail with a name error. Sequence is the default control flow; selection and iteration are the two ways of departing from straight-line execution.
A variable is a named store whose value can change during execution. A constant is a named value that is fixed once set and must not change. Using a constant for something that should never vary — a VAT rate, the value of pi, a maximum number of attempts — makes intent clear and prevents accidental modification.
# Convention in Python: UPPER_CASE names signal "treat as constant"
MAX_ATTEMPTS = 3 # a constant: should never be reassigned
VAT_RATE = 0.20 # a constant
attempts = 0 # a variable: changes as the user retries
price = 50.0 # a variable
total = price * (1 + VAT_RATE)
# OCR-style pseudocode declares constants explicitly
constant MAX_ATTEMPTS = 3
attempts = 0
| Variable | Constant | |
|---|---|---|
| Value can change? | Yes | No |
| Purpose | Hold data that varies (counts, inputs) | Name a fixed value used in several places |
| Benefit | Flexibility | Clarity of intent; one place to change a fixed value; protection from accidental edits |
Using a named constant such as VAT_RATE rather than scattering the literal 0.20 through the code is good practice: the name documents the meaning, and if the rate ever changes you edit one line. Python enforces constancy only by convention (an UPPER_CASE name); some languages enforce it (final in Java, const in C++).
Selection chooses between alternative paths according to a condition that evaluates to True or False.
score = 72
if score >= 90:
grade = "A*"
elif score >= 80:
grade = "A"
elif score >= 70:
grade = "B"
elif score >= 60:
grade = "C"
else:
grade = "U"
print(f"Grade: {grade}") # Grade: B
Trace score = 72: the first test >= 90 is False; >= 80 is False; >= 70 is True, so grade becomes "B" and — crucially — the remaining branches are skipped. The order matters: because each elif is only reached when the ones above failed, writing the boundaries from highest to lowest is what makes the ranges work. The pseudocode form mirrors this:
# OCR-style pseudocode
IF score >= 90 THEN
grade = "A*"
ELSEIF score >= 80 THEN
grade = "A"
ELSEIF score >= 70 THEN
grade = "B"
ELSE
grade = "U"
ENDIF
For selecting on a single value with many options, some languages (Java, C++) offer a switch statement; OCR pseudocode has the equivalent SWITCH ... CASE:
# OCR-style pseudocode: switch on a single value
SWITCH day:
CASE "Monday":
OUTPUT "Start of the week"
CASE "Friday":
OUTPUT "End of the week"
DEFAULT:
OUTPUT "Midweek"
ENDSWITCH
Python's nearest equivalent is the match statement (3.10+):
match day:
case "Monday":
print("Start of the week")
case "Friday":
print("End of the week")
case _:
print("Midweek")
| Selection form | Best when |
|---|---|
if/elif/else | Conditions are ranges or complex Boolean expressions |
switch/case | Choosing among many discrete values of one variable |
Nested if | A decision depends on the outcome of a previous decision |
Conditions can be combined with the Boolean operators AND, OR and NOT, and selections can be nested — one decision inside the branch of another. The two approaches sometimes express the same logic, and choosing the clearer one matters.
# Nested: an inner decision only reached if the outer one is true
if logged_in:
if is_admin:
print("Welcome, administrator")
else:
print("Welcome, user")
else:
print("Please log in")
# Combined: a single condition using AND
if logged_in and is_admin:
print("Welcome, administrator")
Trace the nested version with logged_in = True, is_admin = False: the outer test passes, so we enter; the inner is_admin test fails, so the else runs, printing "Welcome, user". Nesting is right when the inner question only makes sense once the outer one is settled (you cannot ask "is this user an admin?" before knowing someone is logged in). Combining with and is tidier when you simply need several conditions to hold at once. Deeply nested ifs quickly become hard to follow, so flattening them with combined Boolean conditions — or with an early "guard" that handles the failure case first — is a common readability improvement, and recognising when each form is clearer is the kind of judgement that earns marks on code-quality questions.
Iteration repeats a block of code. There are two families — count-controlled (a known number of repetitions) and condition-controlled (repeat until a condition changes) — and the latter splits by when the condition is tested.
for i in range(1, 11): # i takes 1, 2, ..., 10
print(i)
names = ["Alice", "Bob", "Charlie"]
for name in names: # iterate over a collection
print(f"Hello, {name}")
# OCR-style pseudocode
FOR i = 1 TO 10
OUTPUT i
NEXT i
A for loop suits a predetermined number of iterations — counting to ten, or visiting every element of a list. range(1, 11) produces 1 up to but not including 11, a common off-by-one trap.
The condition is tested before each pass, so the body may run zero times.
password = ""
while password != "secret123": # checked first
password = input("Enter password: ")
print("Access granted")
# OCR-style pseudocode
WHILE password != "secret123"
password = INPUT("Enter password: ")
ENDWHILE
Use a while loop when the number of repetitions is unknown in advance — you stop when something becomes true. If the password were already correct, the body would not execute at all.
The condition is tested after the body, so the body always runs at least once. Python has no built-in form, but it is simulated with while True and break:
# Simulating do-while in Python: body runs, THEN we test
while True:
number = int(input("Enter a positive number: "))
if number > 0:
break
print(f"You entered: {number}")
# OCR-style pseudocode
DO
number = INPUT("Enter a positive number: ")
UNTIL number > 0
This post-check shape is ideal for input validation and menus, where you must prompt at least once before you can test the answer.
| Loop | Condition tested | Minimum runs | Best for |
|---|---|---|---|
| for | Counter-driven | Predetermined | A known number of iterations |
| while | Before each pass | 0 | Unknown count; may not run at all |
| do-while / repeat-until | After each pass | 1 | Must run at least once (menus, validation) |
Exam Tip: Choosing the right loop is a frequent question. Known count → for; unknown count, possibly zero → while; must run once → do-while. Justify by referring to when the condition is tested and the minimum number of iterations.
A subroutine is a named block of code that performs a specific task, called by name from elsewhere. Subroutines deliver code reuse, modularity and readability — and split into two kinds.
| Type | Returns a value? | Purpose |
|---|---|---|
| Procedure | No | Performs an action (printing, updating data) |
| Function | Yes | Computes and returns a value |
def display_menu(): # procedure: does something, returns nothing
print("1. Play Game")
print("2. View Scores")
print("3. Quit")
def calculate_area(length: float, width: float) -> float: # function: returns
return length * width
display_menu() # called as a statement
area = calculate_area(5.0, 3.0) # called as part of an expression
print(f"Area: {area}") # Area: 15.0
# OCR-style pseudocode
PROCEDURE displayMenu()
OUTPUT "1. Play Game"
OUTPUT "2. View Scores"
OUTPUT "3. Quit"
END PROCEDURE
FUNCTION calculateArea(length: REAL, width: REAL) RETURNS REAL
RETURN length * width
END FUNCTION
The telling difference is in how each is used: a procedure is called as a standalone statement (displayMenu()); a function produces a value you do something with (area = calculateArea(...)). A function with a RETURN belongs on the right-hand side of an assignment or inside a larger expression.
Parameters are the inputs a subroutine receives when called. (Strictly, the names in the definition are parameters; the actual values supplied at the call are arguments — a distinction worth knowing.) There are two ways the data can be handed over.
A copy of the value is passed; changes inside the subroutine do not affect the original.
def double(x):
x = x * 2
print(f"Inside: {x}") # Inside: 10
num = 5
double(num)
print(f"Outside: {num}") # Outside: 5 (unchanged)
double received a copy of 5. Doubling x changed only the copy, so num outside is still 5. The original is protected.
A reference (in effect, a pointer) to the original is passed; changes inside the subroutine do affect the original.
def add_item(my_list, item):
my_list.append(item) # mutates the original list
shopping = ["milk", "bread"]
add_item(shopping, "eggs")
print(shopping) # ['milk', 'bread', 'eggs'] (changed!)
Here my_list refers to the same list object as shopping (recall reference semantics from OOP Fundamentals), so appending through it is visible outside. This is by-reference behaviour: the subroutine reached the caller's data.
| Feature | By value | By reference |
|---|---|---|
| What is passed | A copy of the data | A reference to the original |
| Original modified? | No | Yes |
| Memory | Extra memory for the copy | None extra (shares the original) |
| Safety | Safer — original protected | Riskier — original can change |
| Python behaviour | Immutable types (int, str, tuple) | Mutable types (list, dict, set) |
# OCR-style pseudocode: BYREF marks a by-reference parameter
PROCEDURE doubleByValue(x: INTEGER) // by value (default)
x = x * 2
END PROCEDURE
PROCEDURE doubleByRef(BYREF x: INTEGER) // by reference
x = x * 2
END PROCEDURE
Python's behaviour rewards a closer look, because mutating a passed object is not the same as reassigning the parameter. Consider:
def modify(items):
items.append(99) # mutates the shared list -> visible outside
items = [1, 2, 3] # REBINDS the local name -> NOT visible outside
data = [10, 20]
modify(data)
print(data) # [10, 20, 99]
Subscribe to continue reading
Get full access to this lesson and all 10 lessons in this course.