Software Development Steps
🎯 (SE-11-01)
📌 Explore fundamental software development steps used by programmers when designing software
- requirements definition
- determining specifications
- design
- development
- integration
- testing and debugging
- installation
- maintenance
Requirements Definition
The programmer identifies what the software must achieve by consulting stakeholders.Conducting interviews with school staff to determine features for a new attendance portal.
Determining Specifications
Defining technical constraints such as hardware requirements, RAM limits, and supported operating systems.Specifying that a game must run at 60fps on a Raspberry Pi 4.
Design
Algorithms are planned using pseudocode or flowcharts.Creating a structure chart to map out how the 'Calculate Tax' subroutine interacts with the main program.
Development
The design is translated into a programming language like Python.Writing the 'if-else' logic to handle user login authentication.
Integration
Combining individual modules into a cohesive system.Connecting the 'User Interface' module to the 'Database Management' module.
Testing and Debugging
Identifying and rectifying errors using test data.Running a 'Dry Run' to find out why a loop runs one too many times (an off-by-one error).
Installation
Deploying the software to the user.Providing a .dmg or .exe installer and a user manual to the client.
Maintenance
The ongoing process of patching bugs and adding features.Releasing a security patch to protect user data from a newly discovered vulnerability.
Process diagram
The software development steps
The eight steps NESA lists, in order, with maintenance feeding back into new requirements.
What to notice
- Each step produces something the next step needs, such as specifications before design.
- The dashed arrow shows that maintenance often uncovers new requirements, so development is a cycle.
Try thisPick an app you use. Which step would a bug fix after launch belong to?
A mini-project through the steps
Reading the eight steps is easier once you watch one small project pass through all of them. A school canteen asks for a program that totals a lunch order. Follow the canteen order calculator from the first interview to the first patch.
| Step | What the team does for the canteen program | What the step produces |
|---|---|---|
| 1. Requirements definition | Interview the canteen manager and two volunteers. They want to enter each item’s price and quantity, see the order total, give 10% off orders of 10 or more items, and be stopped from typing rubbish. | A list of what the software must do, agreed with the client |
| 2. Determining specifications | Turn the wishes into technical limits: runs on the canteen’s Windows laptop with Python 3, keyboard input only, answers instantly, prices stored as whole cents to avoid rounding errors. | A specification of hardware, software, performance and data formats |
| 3. Design | Sketch the mainline and subroutines in pseudocode (below) and choose the data to store: items, total. | Pseudocode, a structure chart, a data dictionary |
| 4. Development | Write each subroutine in Python from the design. | Source code |
| 5. Integration | Join get_order, apply_discount and format_dollars under one main and check the modules pass the right data to each other. | One working program |
| 6. Testing and debugging | Run test data (9 items, 10 items, no items, the letter “a”), compare actual with expected results and fix what fails. | A test table and a corrected program |
| 7. Installation | Copy the program to the canteen laptop, add a desktop shortcut and hand over a one-page guide. | Software in the client’s hands, with instructions |
| 8. Maintenance | Next term the canteen wants a 15% teacher discount. Change one constant, retest and release version 1.1. | Patches, upgrades and a version history |
The design step for this project produces an uncluttered mainline. Notice that it names each subroutine and shows only the order they run in.
BEGIN Mainline
GetOrder (items, total)
ApplyDiscount (total, items)
DisplayTotal (total)
END MainlineThe development and integration steps turn that design into Python. Each subroutine is written and tried on its own, then main joins them.
DISCOUNT_ITEMS = 10 # orders this size or larger get a discount
DISCOUNT_PERCENT = 10
def read_whole_number(prompt):
"""Keep asking until the user types a whole number of 0 or more."""
while True:
text = input(prompt)
if text.isdigit():
return int(text)
print("Please enter a whole number of 0 or more.")
def get_order():
items = 0
total = 0 # cents
price = read_whole_number("Price in cents (0 to finish): ")
while price != 0:
quantity = read_whole_number("Quantity: ")
items = items + quantity
total = total + price * quantity
price = read_whole_number("Price in cents (0 to finish): ")
return items, total
def apply_discount(total, items):
if items >= DISCOUNT_ITEMS:
return total - total * DISCOUNT_PERCENT // 100
return total
def format_dollars(cents):
return f"${cents // 100}.{cents % 100:02d}"
def main():
items, total = get_order()
total = apply_discount(total, items)
print("Total:", format_dollars(total))
main()During testing and debugging, a run with six items at 450 cents and four items at 300 cents gives:
Price in cents (0 to finish): 450
Quantity: 6
Price in cents (0 to finish): 300
Quantity: 4
Price in cents (0 to finish): 0
Total: $35.10Check it by hand: 6 × 450 + 4 × 300 = 3900 cents; ten items qualify, so 390 cents come off, leaving 3510 cents, which displays as $35.10. The same program is used again in the testing sections of this page to show boundary, path and abnormal test data.
Why the steps form a cycle
The steps are drawn in order, but real projects loop. A test failure sends the team back to development, a design flaw found during integration sends them back to design, and maintenance requests become the requirements for the next version. The flow is a cycle, not a one-way street. The SDLC Guide compares this ordered view with Waterfall and Agile, which are covered later on this page.
Online Code Collaboration Tools
🎯 (SE-11-03, SE-11-06)
📌 Research and evaluate the prevalence and use of online code collaboration tools
Almost every software team now shares its code online. Tools such as GitHub, GitLab and Bitbucket store a project's code in a shared repository and track every change, so people in different places can work on it at once. GitHub is used by many millions of developers, and a large share of open-source projects live there.
| Feature | Evaluation/Analysis | Industry Example |
|---|---|---|
| Version Control | Allows multiple engineers to work on the same file without overwriting changes. Provides a safety net to revert to previous working states. | Using Git to manage changes in a massive codebase like the Linux kernel. |
| Pull Requests | Enables peer review where code is checked for security and efficiency before being merged. Reviewers often catch logic errors before they reach users. | A team member reading a colleague’s change to a payment calculation before it is merged into the main branch. |
| Branching | Allows the main "stable" code to remain untouched while experimental features are developed in isolation. | Creating a 'beta-feature' branch to test a new AI chatbot in an app. |
Evaluating collaboration tools
| Strengths | Limitations |
|---|---|
| Everyone works from the same up-to-date code | Beginners find branching and merge conflicts confusing |
| Full history: you can see who changed what and roll back mistakes | Needs a reliable internet connection |
| Built-in review, issue tracking and automated testing | Private code is stored on another company's servers |
Prevalence and use
Online code collaboration tools are now the normal way teams share code, from a two-person school project to a large open-source product. They are used to store the code, keep its history, review changes, track bugs and run automated checks. Students meet them in the same way: a shared repository for a group project, with each person working on their own branch.
| Tool | Best known for | Worth noting |
|---|---|---|
| GitHub | Hosting Git repositories, pull requests, issue tracking, automated workflows (GitHub Actions) | Very large public community; free private repositories with limits |
| GitLab | Repository hosting with built-in CI/CD; a pull request is called a merge request | Can be installed on an organisation’s own servers |
| Bitbucket | Git hosting from Atlassian that links to Jira issue tracking | Suits teams already using Atlassian tools |
| Shared online editors (for example Replit or VS Code Live Share) | Several people editing and running the same code at once | Good for pair programming; not a replacement for version control |
A typical workflow
Most teams follow the same loop: copy the repository, make a branch, commit small changes, push the branch and ask for a review. Here is the loop as Git commands, with what each does.
git clone https://github.com/example-team/canteen-app.git # copy the shared repository to your computer
git checkout -b add-teacher-discount # start a new branch so main stays stable
# ... edit apply_discount() and test it ...
git add canteen.py # choose the changed file for the next commit
git commit -m "Add 15% teacher discount" # save a snapshot with a message
git push origin add-teacher-discount # upload the branch, then open a pull requestOn the platform, a teammate reads the pull request, comments on lines they question and approves it. Automated tests can run on every pull request, so a change that breaks the tests is caught before it is merged.
Resolving a merge conflict
A merge conflict happens when two branches change the same lines. Git cannot decide which version is right, so it marks both in the file and waits for a person to choose.
def apply_discount(total, items):
<<<<<<< HEAD
if items >= 10:
=======
if items >= 8:
>>>>>>> add-teacher-discount
return total - total * 10 // 100Everything between <<<<<<< HEAD and ======= is the version on the current branch; everything down to >>>>>>> is the incoming version. The developer keeps one version (or combines them), deletes the marker lines, tests, then commits. Talking to the teammate first is often faster than guessing which threshold is right.
Evaluating a tool for your team
| Criterion | Questions to ask |
|---|---|
| Fit to the task | Does it provide version control, review and issue tracking, or only one of them? |
| Learning curve | Can every team member use branching and merging after one lesson? Is there a graphical option? |
| Cost and limits | Are private repositories, storage and automation free for a school team? |
| Privacy and security | Where is the code stored, and who can see it? Never commit passwords or API keys; remove one that slips in and change it. |
| Integration | Does it connect to the team’s editor, testing and project-tracking tools? |
Key Features of Standard Algorithms
🎯 (SE-11-02)
📌 Apply computational thinking and algorithmic design by defining the key features of standard algorithms, including sequence, selection, iteration and identifying data that should be stored
Every algorithm uses fundamental building blocks to solve problems. Understanding these features allows programmers to design robust, efficient solutions:
1. Sequence
Sequence means instructions are executed in order, one after another. Each step completes before the next begins — there is no branching or repetition.Reading a file, processing each line, then saving results.
Flowchart
Sequence: calculate the area of a rectangle
Steps run one after another, in the order they are written. There are no decisions and no loops.
What to notice
- Every flowchart starts and ends with a terminator.
- Input and output use the parallelogram; calculations use the process rectangle.
Try thisAdd steps to also calculate and output the perimeter. Where do they go?
BEGIN
OUTPUT "Enter the length: "
INPUT length
OUTPUT "Enter the width: "
INPUT width
area ← length * width
OUTPUT "The area is: ", area
END
2. Selection
Selection means choosing different execution paths based on conditions using IF … ELSE, nested IF or a multi-way CASEWHERE structure. The program evaluates a condition and branches accordingly.Validating user input — if age < 18, display "Too young"; otherwise, proceed.
Flowchart
Binary selection: check a user's age
A decision chooses one of two paths. This is IF … THEN … ELSE … ENDIF.
What to notice
- Both arrows leaving the decision are labelled, as NESA requires.
- The two paths join again before END, so exactly one output happens.
Try thisWrite the matching pseudocode using IF … THEN … ELSE … ENDIF.
BEGIN
OUTPUT "Enter your age: "
INPUT age
IF age >= 18 THEN
OUTPUT "Welcome!"
ShowFullMenu ()
ELSE
OUTPUT "Too young - access denied."
ENDIF
END
Two more selection forms appear in NESA’s Course Specifications. Multi-way selection chooses one of several cases by evaluating a single expression, and only one branch runs. It is written CASEWHERE … OTHERWISE … END CASE.
CASEWHERE menuChoice evaluates to
1: OUTPUT "New game"
2: OUTPUT "Load game"
3: OUTPUT "High scores"
OTHERWISE: OUTPUT "Invalid choice"
END CASENested IF tests several conditions in turn when only one process should run. Each further test is a complete IF indented inside the ELSE of the one before it, and every IF has its own ENDIF.
IF mark >= 85 THEN
OUTPUT "High"
ELSE
IF mark >= 65 THEN
OUTPUT "Medium"
ELSE
IF mark >= 50 THEN
OUTPUT "Pass"
ELSE
OUTPUT "Fail"
ENDIF
ENDIF
ENDIFIF lines need three ENDIF lines, closed from the inside out. If the counts do not match, the structure is wrong. Python’s elif expresses the same nested decision in shorter form.3. Iteration
Iteration means repeating a set of instructions while a condition is true, using FOR or WHILE loops. The loop body executes multiple times until the exit condition is met.Summing all numbers in a list by looping through each element.
Flowchart
Pre-test repetition: sum a list
A WHILE loop tests its condition before each pass, so the body may run zero times.
What to notice
- The arrow from the loop body returns to the decision, not to the start.
- total is an accumulator: it keeps its value between passes.
Try thisChange the flowchart to find the largest value instead of the total.
BEGIN
numbers ← [10, 25, 3, 47, 8]
total ← 0
FOR i = 0 TO LENGTH(numbers) - 1
total ← total + numbers[i]
NEXT i
OUTPUT "The sum is: " + total
END
Interactive trace
Accumulator Loop (Sum a List)
Watch the flowchart, the pseudocode and the variables change together.
What to notice
The running total is an accumulator: it starts at 0 and grows by one item each time round the loop. The i variable is the loop counter.
Step with ◀ and ▶ (or the arrow keys), play or pause with Space, reset with R.
Try thisEnter your own list and predict the final total before pressing Play. How many loop tests does a list of n items need? (Answer: n + 1 — the last test is the one that fails.)
NESA describes three loops. WHILE is a pre-test loop, so its body may never run. REPEAT … UNTIL is a post-test loop, so its body always runs at least once, which suits input validation. FOR … NEXT is a counted loop; STEP sets the increment and can be negative.
| Loop | Tests the condition | Body runs | Use it when |
|---|---|---|---|
WHILE … ENDWHILE | Before each pass | Zero or more times | You do not know how many passes, and none may be needed (read until a sentinel) |
REPEAT … UNTIL | After each pass | At least once | The body must run first, for example asking for a value until it is valid |
FOR … NEXT | Counts from start to finish | A known number of times | You know the count, or you are visiting every position of an array |
REPEAT … UNTIL
REPEAT
OUTPUT "Enter a mark from 0 to 100: "
INPUT mark
UNTIL mark >= 0 AND mark <= 100FOR … NEXT with STEP
FOR i = 1 TO 10 STEP 2
OUTPUT i
NEXT i
// Output: 1 3 5 7 94. Storing Data
Storing Data means using variables and data structures to hold values needed across multiple steps of an algorithm. Without stored state, programs cannot track progress or accumulate results.Keeping track of a running total as you iterate through numbers.
Flowchart
Storing data: average of numbers until −1
Variables remember values across the loop, so the program can calculate an average at the end.
What to notice
- −1 is a sentinel value: it ends input but is never added to the total.
- Checking count > 0 prevents a division-by-zero runtime error.
Try thisDesk check it with the inputs 10, 20, 30, −1. Record total and count after each pass.
BEGIN
runningTotal ← 0
count ← 0
OUTPUT "Enter numbers (-1 to finish): "
INPUT number
WHILE number != -1
runningTotal ← runningTotal + number
count ← count + 1
INPUT number
ENDWHILE
IF count > 0 THEN
average ← runningTotal / count
OUTPUT "Average: " + average
ELSE
OUTPUT "No data entered."
ENDIF
END
Identifying the data that should be stored
Before writing steps, decide what the algorithm must remember. Three questions find every stored item: what comes in (inputs), what must be carried from one step to the next (state such as totals, counters and “best so far”), and what goes out (outputs). Anything that does not need to survive to the next step can be a temporary value.
Worked example: the highest mark and the number of passes
Problem: marks are typed in one at a time until -1 is entered. Report the highest mark and how many students scored 50 or more.
| Data | Kind | Why it is stored | Type |
|---|---|---|---|
mark | Input | The value just entered; the loop tests it | Integer |
highest | State | The largest mark so far, compared with each new mark | Integer, starts at 0 |
passCount | State | A running count of marks of 50 or more | Integer, starts at 0 |
Sentinel -1 | Constant | Tells the loop that input has finished; never counted as a mark | Integer |
BEGIN FindHighest
highest ← 0
passCount ← 0
INPUT mark
WHILE mark != -1
IF mark > highest THEN
highest ← mark
ENDIF
IF mark >= 50 THEN
passCount ← passCount + 1
ENDIF
INPUT mark
ENDWHILE
OUTPUT "Highest: ", highest
OUTPUT "Passes: ", passCount
END FindHighestThis one algorithm uses all three structures: sequence (initialise, loop, report), selection (two independent IF tests) and iteration (the WHILE loop). A desk check with the marks 72, 45, 91, 50 records the stored values after each pass:
| Input mark | mark > highest? | highest | mark >= 50? | passCount |
|---|---|---|---|---|
| (start) | 0 | 0 | ||
| 72 | Yes | 72 | Yes | 1 |
| 45 | No | 72 | No | 1 |
| 91 | Yes | 91 | Yes | 2 |
| 50 | No | 91 | Yes | 3 |
| -1 | loop ends | 91 | 3 |
The same design in Python reads the marks from a list, so it can be run and checked straight away.
marks = [72, 45, 91, 50]
highest = 0
pass_count = 0
for mark in marks:
if mark > highest:
highest = mark
if mark >= 50:
pass_count = pass_count + 1
print("Highest:", highest)
print("Passes:", pass_count)Highest: 91
Passes: 3Computational thinking
Computational thinking is the habit behind every design above. It has four parts, and you use all four whenever you plan an algorithm.
IF/ENDIF, WHILE/ENDWHILE, FOR/NEXT) and indent each structure. Marks are lost for a loop that never changes its condition, an accumulator that is not initialised, and mixing up WHILE and REPEAT … UNTIL.By combining sequence, selection, iteration and effective data storage, algorithms become clear, testable and maintainable.
A clear introduction to what an algorithm is and why efficiency matters — using everyday examples like sorting and searching.
Divide and Conquer, and Backtracking
🎯 (SE-11-02)
📌 Apply divide and conquer and backtracking as algorithmic design strategies
Divide and Conquer
This strategy involves breaking a complex problem into smaller, identical sub-problems until they are simple enough to solve directly. The solutions to these sub-problems are then combined to form the final result.In a Merge Sort, a large list is repeatedly split in half until single elements remain, which are then merged back together in the correct order. This approach is highly efficient for sorting and searching large datasets.
The three steps
- Divide the problem into smaller sub-problems of the same kind.
- Conquer each sub-problem, by solving it directly when it is small enough (the base case) or by applying the same method again.
- Combine the sub-solutions into the answer to the original problem. Binary search needs no combining because it discards the half that cannot contain the target.
Worked example: binary search
Binary search finds a target in a sorted list by checking the middle item and discarding the half that cannot contain it. NESA-style pseudocode for the algorithm:
BEGIN BinarySearch (list, target)
low ← 0
high ← LENGTH(list) - 1
WHILE low <= high
mid ← whole number part of (low + high) / 2
IF list[mid] = target THEN
RETURN mid
ELSE
IF list[mid] < target THEN
low ← mid + 1
ELSE
high ← mid - 1
ENDIF
ENDIF
ENDWHILE
RETURN -1
END BinarySearch (list, target)Trace it with the sorted list [12, 19, 25, 31, 44, 57, 63, 78, 90] (indexes 0 to 8) and the target 57:
| Pass | low | high | mid | list[mid] | Comparison | Action |
|---|---|---|---|---|---|---|
| 1 | 0 | 8 | 4 | 44 | 44 < 57 | Discard the left half: low ← 5 |
| 2 | 5 | 8 | 6 | 63 | 63 > 57 | Discard the right half: high ← 5 |
| 3 | 5 | 5 | 5 | 57 | 57 = 57 | Found: RETURN 5 |
Three comparisons found the target. Linear search would have needed six. For a target that is absent, such as 50, the same three passes end with low greater than high and the algorithm returns -1. Step through binary search yourself in the interactive trace in the next section.
def binary_search(items, target):
low = 0
high = len(items) - 1
while low <= high:
mid = (low + high) // 2
if items[mid] == target:
return mid
elif items[mid] < target:
low = mid + 1
else:
high = mid - 1
return -1
marks = [12, 19, 25, 31, 44, 57, 63, 78, 90]
print(binary_search(marks, 57))
print(binary_search(marks, 50))5
-1Worked example: merge sort
Merge sort divides until every list has one item (a list of one item is already sorted, so this is the base case), then combines by merging two sorted lists into one. Here the list [38, 27, 43, 3, 9, 82, 10] is divided and then combined:
| Phase | Lists at this stage |
|---|---|
| Start | [38, 27, 43, 3, 9, 82, 10] |
| Divide | [38, 27, 43] and [3, 9, 82, 10] |
| Divide again | [38] [27, 43] and [3, 9] [82, 10] |
| Base case: single items | [38] [27] [43] and [3] [9] [82] [10] |
| Merge pairs | [27, 43] and [3, 9] and [10, 82] |
| Merge again | [27, 38, 43] and [3, 9, 10, 82] |
| Final merge | [3, 9, 10, 27, 38, 43, 82] |
def merge_sort(items):
if len(items) <= 1: # base case
return items
middle = len(items) // 2
left = merge_sort(items[:middle]) # divide and conquer each half
right = merge_sort(items[middle:])
merged = []
i = j = 0
while i < len(left) and j < len(right): # combine: merge two sorted lists
if left[i] <= right[j]:
merged.append(left[i])
i = i + 1
else:
merged.append(right[j])
j = j + 1
return merged + left[i:] + right[j:]
print(merge_sort([38, 27, 43, 3, 9, 82, 10]))[3, 9, 10, 27, 38, 43, 82]Why it is efficient
| Items in the list | Linear search, worst case | Binary search, worst case |
|---|---|---|
| 9 | 9 comparisons | 4 comparisons |
| 1 000 | 1 000 comparisons | 10 comparisons |
| 1 000 000 | 1 000 000 comparisons | 20 comparisons |
Each pass halves the range, so doubling the list adds only one more comparison. That is why binary search is described as O(log n) and linear search as O(n). The price is the requirement that the data is already sorted. Merge sort takes roughly n log n steps, far fewer than the n² of bubble sort on a large list.
Backtracking
Backtracking is an incremental approach to problem-solving that builds a solution one step at a time. If the algorithm reaches a state where the current path cannot possibly lead to a valid solution (violating a constraint), it "backtracks" to the previous step and tries a different option.A Sudoku Solver attempts a number in a cell; if it later finds a conflict, it returns to that cell and tries the next available number. This strategy is essential for pathfinding and optimisation problems.
# Divide and Conquer: Recursive Binary Search
def binary_search(arr, target, low, high):
if low > high: return -1
mid = (low + high) // 2
if arr[mid] == target: return mid
elif arr[mid] > target:
return binary_search(arr, target, low, mid - 1)
else:
return binary_search(arr, target, mid + 1, high)
# Backtracking: Maze Pathfinding (tries 4 directions)
visited = set()
def find_path(x, y, maze):
if is_exit(x, y): return True
if is_wall(x, y) or (x, y) in visited: return False
visited.add((x, y)) # Mark as visited
# Try all 4 directions: right, down, left, up
for dx, dy in [(1, 0), (0, 1), (-1, 0), (0, -1)]:
if find_path(x + dx, y + dy, maze):
return True
return False # Backtrack to previous cell
Worked example: a maze, step by step
The maze below has a wrong turn built in. The search tries directions in the order right, down, left, up. Watch the call stack on the right: each recursive call adds a line, and a dead end removes lines as the function returns False and control backs up to an earlier choice.
Animated diagram
Backtracking out of a dead end
A recursive search tries right, down, left, up, and undoes a wrong turn when nothing is left to try.
What to notice
- The dead end (red) is only discovered by trying it; nothing warned the function in advance.
- Backtracking is not starting again. The search returns to the most recent junction that still has an untried direction.
- Visited squares stay marked, so no square is searched twice.
Step with ◀ and ▶ (or the arrow keys), play or pause with Space.
Try thisChange the order of directions to up, left, down, right. Predict which squares would be visited before deciding, then check with the Python below.
The complete function below matches the animation. The variable path collects the squares on the winning route as each call returns True, so it is built from the exit back to the start.
MAZE = ["...#.",
"##.#.",
".....",
".####",
"....."]
ROWS, COLS = len(MAZE), len(MAZE[0])
visited = set()
path = []
def find_path(x, y):
"""Return True if the exit (bottom-right) can be reached from (x, y)."""
if (x, y) == (COLS - 1, ROWS - 1):
path.append((x, y))
return True # base case: exit found
if not (0 <= x < COLS and 0 <= y < ROWS):
return False # off the grid
if MAZE[y][x] == "#" or (x, y) in visited:
return False # wall, or already tried
visited.add((x, y))
for dx, dy in [(1, 0), (0, 1), (-1, 0), (0, -1)]: # right, down, left, up
if find_path(x + dx, y + dy):
path.append((x, y))
return True
return False # dead end: backtrack
print(find_path(0, 0))
print(path[::-1])True
[(0, 0), (1, 0), (2, 0), (2, 1), (2, 2), (1, 2), (0, 2), (0, 3), (0, 4), (1, 4), (2, 4), (3, 4), (4, 4)]Worked example: four queens
Place four queens on a 4 × 4 board so that no two share a row, column or diagonal. Backtracking places one queen per row, and undoes the last placement as soon as a row has no safe column. The trace shows the attempts in order:
| Step | What the search does | Result |
|---|---|---|
| 1 | Row 1: try column 1 | Safe, placed |
| 2 | Row 2: try columns 1, 2, 3 | Columns 1 and 2 are attacked; column 3 is safe, placed |
| 3 | Row 3: try columns 1 to 4 | Every column is attacked: dead end, so backtrack and remove the queen from row 2 |
| 4 | Row 2: try column 4 | Safe, placed |
| 5 | Row 3: try columns 1, 2 | Column 1 is attacked; column 2 is safe, placed |
| 6 | Row 4: try columns 1 to 4 | Every column is attacked: dead end, so backtrack; row 3 has no other safe column, so remove row 2’s queen as well |
| 7 | Row 2 has no more columns, so remove row 1’s queen. Row 1: try column 2 | Safe, placed |
| 8 | Rows 2, 3, 4 | Row 2 column 4, row 3 column 1, row 4 column 3 are all safe. Solution found |
def is_safe(columns, row, col):
"""columns[r] is the column of the queen already placed in row r."""
for r, c in enumerate(columns):
if c == col or abs(c - col) == row - r: # same column or same diagonal
return False
return True
def place_queens(columns, size):
row = len(columns)
if row == size:
return True # every row has a queen
for col in range(size):
if is_safe(columns, row, col):
columns.append(col) # try this square
if place_queens(columns, size):
return True
columns.pop() # undo it: backtrack
return False
columns = []
place_queens(columns, 4)
print([c + 1 for c in columns])
for col in columns:
print(" ".join("Q" if c == col else "." for c in range(4)))[2, 4, 1, 3]
. Q . .
. . . Q
Q . . .
. . Q .Both examples share a pattern you can reuse: choose an option, check the constraint, recurse to the next choice, and undo the choice if the recursion fails.
Choosing between the two strategies
| Divide and conquer | Backtracking | |
|---|---|---|
| Core idea | Split into smaller versions of the same problem, solve, combine | Build a solution step by step and undo a step when it breaks a constraint |
| Sub-problems | Independent; each half is solved on its own | Dependent; each choice affects the choices after it |
| Examples | Binary search, merge sort | Maze paths, four queens, Sudoku |
| Cost | Fast because the problem shrinks quickly | Can be slow because many dead ends may be tried |
low, high and mid for every pass. For backtracking, show each dead end and the step where the choice is undone. A full-mark answer also names the base case that stops the recursion. A common mistake is describing backtracking as “trying everything at random” when it abandons a path as soon as it fails a constraint.Recursive Functions
A recursive function is one that calls itself. Every recursive function must have:
- Base case: The condition that stops the recursion (no more recursive calls).
- Recursive case: Where the function calls itself with a simpler input, moving toward the base case.
RecursionError: maximum recursion depth exceeded (stack overflow).
# Iterative approach
def factorial_iterative(n):
result = 1
for i in range(1, n + 1):
result *= i
return result
# Recursive approach
def factorial_recursive(n):
if n == 0 or n == 1: # Base case
return 1
return n * factorial_recursive(n - 1) # Recursive case
print(factorial_iterative(5)) # 120
print(factorial_recursive(5)) # 120
# Call stack for factorial_recursive(4):
# factorial(4) → 4 × factorial(3)
# factorial(3) → 3 × factorial(2)
# factorial(2) → 2 × factorial(1)
# factorial(1) → returns 1 ← base case reached
# factorial(2) → returns 2
# factorial(3) → returns 6
# factorial(4) → returns 24
def fibonacci(n):
if n <= 1: # Base cases: fib(0)=0, fib(1)=1
return n
return fibonacci(n - 1) + fibonacci(n - 2)
for i in range(8):
print(fibonacci(i), end=" ")
# Output: 0 1 1 2 3 5 8 13
Walks through statements, conditionals, loops, and how code is packaged into reusable functions — the building blocks covered in this section.
Pseudocode and Flowcharts
🎯 (SE-11-02)
📌 Develop structured algorithms using pseudocode and flowcharts, including the use of subprograms
Pseudocode
Pseudocode is an informal, human-readable notation for writing algorithms — a bridge between natural language and programming code. It uses common programming keywords (IF, WHILE, FOR) mixed with English-like descriptions, making logic crystal clear without being tied to a specific programming language's syntax. This allows designers and developers to communicate algorithm logic to non-programmers, and to verify correctness before investing time in actual coding.Example:
BEGIN SearchList (list, target)
FOR index = 0 TO LENGTH(list) - 1
IF list[index] = target THEN
RETURN index
ENDIF
NEXT index
RETURN -1 // not found
END SearchList (list, target)
Flowcharts
Flowcharts use visual symbols to represent algorithm steps and decision points. Rectangles represent processes, diamonds represent decisions, and arrows show flow direction. This visual representation makes complex logic easy to follow, helps identify loops and branches, and allows stakeholders to validate correctness before implementation.Example: A flowchart for validating user age would show a diamond (decision point) — "Is age ≥ 18?" — with two paths: YES leads to "Show full menu" and NO leads to "Show restricted menu."
NESA pseudocode conventions
NESA’s Course Specifications set the rules for pseudocode in the exam. Learn them as a checklist:
- Keywords are written in capitals.
- Structural elements come in pairs: every
BEGINhas anEND, everyIFhas anENDIF, everyWHILEanENDWHILE, everyFORaNEXT. - Indentation shows which lines belong to which control structure.
- When refining a solution, a subroutine can be referred to by name, with a separate algorithm of the same name giving its detail.
| Structure | NESA pseudocode |
|---|---|
| Sequence | process 1 process 2 process 3 |
| Binary selection | IF condition THEN
process 1
ELSE
process 2
ENDIF |
| Multi-way selection | CASEWHERE expression evaluates to
choice a: process a
choice b: process b
OTHERWISE: default process
END CASE |
| Nested IF | IF condition A THEN
process 1
ELSE
IF condition B THEN
process 2
ELSE
process 3
ENDIF
ENDIF |
| Pre-test loop | WHILE condition is true
process
ENDWHILE |
| Post-test loop | REPEAT
process
UNTIL condition is true |
| Counted loop | FOR variable = start TO finish STEP increment
statements
NEXT variable |
| Subroutine and function | BEGIN name (parameters)
statements
RETURN value // functions only
END name (parameters) |
ENDIF, ENDWHILE and NEXT. Write a nested IF as an IF inside the ELSE of the previous one, with one ENDIF per IF.From flowchart to pseudocode
| Flowchart symbol | What it means | Pseudocode it becomes |
|---|---|---|
| Terminator (rounded) | Start or end of an algorithm or subroutine | BEGIN and END |
| Parallelogram | Input or output | INPUT age, OUTPUT total |
| Rectangle | A process such as a calculation or assignment | total ← total + price |
| Diamond | A decision with labelled exits | IF … ENDIF, or the test of a loop |
| Rectangle with double side lines | A call to a subprogram | The subroutine’s name and arguments: DisplayTotal (total) |
Reference
Flowchart symbols
The five symbols in the NESA course specifications. Use only these, joined by arrows.
- Terminator
- Process
- Decision
- Input or output
- Subprogram
Flowchart
A mainline that calls a subprogram
A clear mainline refers to a subroutine by name; the subroutine's own flowchart shows the detail.
What to notice
- The subprogram symbol has double lines at each side.
- The same subroutine is called twice with different parameters.
- The subroutine's terminators use its name, so it is easy to match to the call.
Try thisAdd a third call that reads a phone number. Does the subroutine need to change?
Tracing Searching and Sorting Algorithms
A desk check follows an algorithm one step at a time, recording each variable as it changes. The traces below do the same thing: the flowchart node, the pseudocode line and the data move together. Step backwards whenever you want to ask "why did it go no there?", and watch the Comparisons counter — it is how we measure an algorithm's efficiency (Big-O).
Interactive trace
Linear Search
Check each item in order until the target is found or the list runs out.
What to notice
Linear search works on any list, sorted or not. In the worst case (the target is last, or absent) it makes one comparison per item: O(n).
Step with ◀ and ▶ (or the arrow keys), play or pause with Space, reset with R.
Try thisSearch for 8, which is not in the list, so you can see the RETURN −1 path. Then compare the number of comparisons for 7 (first) and 5 (last).
Interactive trace
Binary Search
Halve the search range each time — the list must already be sorted.
What to notice
Greyed cells have been ruled out. Each comparison discards half of the remaining range, so a list of n items needs at most about log₂(n) + 1 comparisons: O(log n).
Step with ◀ and ▶ (or the arrow keys), play or pause with Space, reset with R.
Try thisSearch for 10 (absent) and count the comparisons. Then trace linear search for the same value in the same eight items and compare the counts.
Interactive trace
Bubble Sort
Swap neighbouring pairs until a whole pass makes no swaps.
What to notice
After each pass the largest remaining value has "bubbled" to the end (shown in green), so n shrinks by one. The swapped flag stops the sort early once the list is in order.
Step with ◀ and ▶ (or the arrow keys), play or pause with Space, reset with R.
Try thisTry the sorted list: one pass with no swaps and the sort finishes (best case, O(n)). Then try the reversed list for the worst case, O(n²).
Subprograms
A subprogram (function or procedure) is a reusable block of code that performs a specific task. Pseudocode and flowcharts must clearly show how subprograms are called, what parameters they accept, and what they return. This modular approach reduces code duplication, improves readability, and makes testing easier.A main program calls FUNCTION validatePassword(pwd) which returns TRUE or FALSE, allowing the validation logic to be reused across multiple entry points.
NESA highly emphasizes that complex algorithms must start with an uncluttered mainline. This means the main function should read like a table of contents, delegating specific processing logic to clearly named subroutines.
// ❌ Cluttered (Poor Practice)
BEGIN
// Gathering input
OUTPUT "Enter your score out of 100"
INPUT score
WHILE score < 0 OR score > 100
OUTPUT "Invalid. Please enter a valid score: "
INPUT score
ENDWHILE
// Processing logic
IF score >= 85 THEN
grade ← "High Distinction"
ELSE
IF score >= 75 THEN
grade = "Distinction"
ELSE
... (and so on) ...
ENDIF
ENDIF
// Output
OUTPUT "Your grade is ", grade
END
In contrast, an uncluttered mainline is completely modularized, significantly reducing complexity:
// ✅ NESA-Standard Uncluttered Mainline
BEGIN Mainline
score ← GetValidScore ()
grade ← CalculateGrade (score)
DisplayResult (grade)
END Mainline
// Each subroutine is developed separately, using the same name.
// The next three algorithms give the detail.
The three subroutines named in the mainline are developed next. Each is short, does one job and is named for it. The pseudocode is on the left and the same logic in Python is on the right.
NESA pseudocode
BEGIN GetValidScore ()
OUTPUT "Enter your score out of 100: "
INPUT score
WHILE score < 0 OR score > 100
OUTPUT "Invalid. Enter a valid score: "
INPUT score
ENDWHILE
RETURN score
END GetValidScore ()Python
def get_valid_score():
score = int(input("Enter your score out of 100: "))
while score < 0 or score > 100:
score = int(input("Invalid. Enter a valid score: "))
return scoreNESA pseudocode
BEGIN CalculateGrade (score)
IF score >= 85 THEN
grade ← "High Distinction"
ELSE
IF score >= 75 THEN
grade ← "Distinction"
ELSE
IF score >= 50 THEN
grade ← "Pass"
ELSE
grade ← "Fail"
ENDIF
ENDIF
ENDIF
RETURN grade
END CalculateGrade (score)Python
def calculate_grade(score):
if score >= 85:
return "High Distinction"
elif score >= 75:
return "Distinction"
elif score >= 50:
return "Pass"
else:
return "Fail"NESA pseudocode
BEGIN DisplayResult (grade)
OUTPUT "Your grade is ", grade
END DisplayResult (grade)Python
def display_result(grade):
print("Your grade is", grade)
def main():
score = get_valid_score()
grade = calculate_grade(score)
display_result(grade)
main()Enter your score out of 100: 78
Your grade is DistinctionPassing data in and out
A subroutine that needs data receives it through parameters. A procedure carries out an action; a function is a subroutine that passes a single value back with RETURN. These three short examples show one parameter, several parameters, and a returned value.
One parameter
BEGIN ShowGreeting (name)
OUTPUT "Welcome, ", name, "!"
END ShowGreeting (name)
BEGIN Mainline
ShowGreeting ("Alice")
ShowGreeting ("Ben")
END MainlinePython
def show_greeting(name):
print("Welcome, " + name + "!")
show_greeting("Alice")
show_greeting("Ben")Welcome, Alice!
Welcome, Ben!Several parameters, result passed back through a parameter
BEGIN Multiply (a, b, product)
product ← a * b
END Multiply (a, b, product)
BEGIN Mainline
FOR i = 1 TO 3
Multiply (i, 4, answer)
OUTPUT answer
NEXT i
END MainlinePython: an integer cannot be changed by the subroutine, so it returns the value
def multiply(a, b):
return a * b
for i in range(1, 4):
answer = multiply(i, 4)
print(answer)4
8
12A function that returns one value
BEGIN CalculateGST (price)
RETURN price * 0.10
END CalculateGST (price)
BEGIN Mainline
total ← 100 + CalculateGST (100)
OUTPUT total
END MainlinePython
def calculate_gst(price):
return price * 0.10
total = 100 + calculate_gst(100)
print(total)110.0BEGIN name (parameters) … END name (parameters). Show every parameter the subroutine receives, and use RETURN when it is a function. Check that every keyword has its matching partner and that indentation shows the structure. Do not mix flowchart symbols with pseudocode in the same answer unless the question asks for both.Modelling Tools for Top-Down and Bottom-Up Design
🎯 (SE-11-02)
📌 Use modelling tools including structure charts, abstraction and refinement diagrams to support top-down and bottom-up design
Structure Charts
Structure charts are a top-down tool used to show the hierarchy of subroutines in a system. They focus on modularity, showing which module calls another and what data (parameters) or control signals (flags) are passed between them.A structure chart for an ATM would show the 'Main' module calling 'Verify PIN' and passing back a 'Valid/Invalid' flag. This allows engineers to see high-level dependencies without getting bogged down in code logic.
Structure chart
Structure chart: an ATM system
The system broken into modules, with the data and flags passed between them.
What to notice
- An open circle on an arrow is data (a parameter); a filled circle is a flag.
- The curved arrow shows the main module repeats until the card is removed.
- The diamond shows only one of Withdraw, Deposit or Check balance runs each time.
Try thisAdd a Print receipt module that runs only if the customer asks. Which symbol shows that?
Abstraction and Refinement
Abstraction involves hiding implementation details to focus on high-level goals (the 'What'). Stepwise Refinement is the process of breaking those high-level goals into smaller, more specific details (the 'How').An abstract instruction like 'Initialise System' is refined into 'Set counter to 0', 'Clear display', and 'Open file stream'. Together, they allow for manageable development of complex systems.
Top-down and bottom-up design
Top-down design starts with the whole problem and breaks it into smaller and smaller subroutines until each is simple enough to code. Bottom-up design starts with small, tested building blocks (or existing library functions) and joins them into larger parts. Most real projects use both: plan from the top, but build on reliable pieces at the bottom.
| Top-down | Bottom-up | |
|---|---|---|
| Starts with | The overall goal | Small, well-understood components |
| Tool that supports it | Structure chart, then refinement of each box | A list of available modules and functions, joined by a mainline |
| Strength | Keeps the whole system in view; easy to divide work in a team | Reuses tested code; early progress on the hard parts |
| Risk | Detail is left until late, so a hard part may surprise you | Pieces may not fit together into the required whole |
| Example | Canteen program: Get order, Apply discount, Display total, then refine each | Reuse Python’s isdigit and input to build read_whole_number, then build up from there |
Reading a structure chart
The structure chart in the ATM figure uses four symbols from NESA’s Course Specifications. Each one answers a question about how the modules relate:
pin or amount.valid or cardRemoved, passed between subroutines.Worked example: refining the canteen program
Stepwise refinement works down through levels, adding detail only where it is needed. Each level is a complete, readable algorithm on its own. Level 0 is the mainline; level 1 refines GetOrder; level 2 refines the input check.
Level 0: mainline
BEGIN Mainline
GetOrder (items, total)
ApplyDiscount (total, items)
DisplayTotal (total)
END MainlineLevel 1: refine GetOrder
BEGIN GetOrder (items, total)
items ← 0
total ← 0
ReadWholeNumber (price)
WHILE price != 0
ReadWholeNumber (quantity)
items ← items + quantity
total ← total + price * quantity
ReadWholeNumber (price)
ENDWHILE
END GetOrder (items, total)BEGIN ReadWholeNumber (number)
REPEAT
INPUT text
UNTIL text is a whole number of 0 or more
number ← text as a number
END ReadWholeNumber (number)The same hierarchy is what the structure chart shows: Mainline at the top calling GetOrder, ApplyDiscount and DisplayTotal, with GetOrder calling ReadWholeNumber repeatedly (a curved arrow), and items and total passed back up as data (empty circles). A refinement level can be drawn as its own structure chart with the same name as the box it refines.
Verify PIN), put the empty circle on data and the filled circle on flags, and show the diamond only where a call is optional. When asked to show refinement, give the higher level and a lower-level algorithm with the same name. Marks are lost for drawing flowchart symbols in a structure chart, or for putting decisions inside the boxes instead of on the connecting lines.Analysing the Logic of Written Algorithms
🎯 (SE-11-02)
📌 Analyse the logic and structure of written algorithms
- determining inputs and outputs
- determining the purpose of the algorithm
- desk checking and peer checking
- determining connections of written algorithms to other subroutines or functions
Determining Inputs, Outputs, and Purpose
Before writing code, clearly identify what data the algorithm needs (inputs), what it produces (outputs), and why it exists (purpose). This clarity prevents bugs and ensures the solution addresses the right problem.A sorting algorithm has INPUT: unsorted list; OUTPUT: sorted list; PURPOSE: arrange items in ascending order for efficient searching.
Desk Checking and Peer Checking
Desk checking (or "dry running") involves manually tracing through an algorithm step-by-step with sample data, recording the value of each variable at each step to verify correctness before implementation. Peer checking is having another person review the algorithm logic to catch errors or inefficiencies that the original author might miss.Desk checking a loop from 1 to 10 — write down i=1, i=2, ... to verify the loop executes exactly 10 times, not 9 or 11.
Identifying Connections to Subroutines and Functions
Analyse how an algorithm calls other functions and what data is passed between them. Document the purpose of each function call and the parameters it receives, ensuring data flows correctly through the program.An algorithm that calculates employee pay might call FUNCTION calculateTax(salary) and FUNCTION applyBonus(basePay, bonusPercent), understanding what each returns and how it affects the final result.
BEGIN Mystery (list)
result ← list[0]
FOR i = 1 TO LEN(list) - 1
IF list[i] > result THEN
result ← list[i]
ENDIF
NEXT i
RETURN result
END Mystery (list)
A desk check with [4, 9, 2] gives result = 4, then 9, then 9. Input: a list of numbers. Output: one number. Purpose: it finds the largest value.Clues that reveal purpose
| Look at… | It tells you… | In Mystery |
|---|---|---|
| Parameters and inputs | What data the algorithm works on | A list of numbers |
| Starting values | What is being accumulated or compared | result starts as the first item, so it tracks a “best so far” |
| The test inside the loop | What counts as better or a match | list[i] > result: bigger wins |
| What is returned | The output | A single number, the largest |
Worked desk check: finding a logic error
An algorithm should count the marks of 50 or more in a list. It compiles and runs, so a desk check with chosen data is how the fault is found. The array positions run from 1 in this algorithm.
BEGIN CountPasses (marks)
count ← 0
FOR i = 1 TO LENGTH(marks)
IF marks[i] > 50 THEN
count ← count + 1
ENDIF
NEXT i
RETURN count
END CountPasses (marks)Test data: marks = [45, 60, 50]. Expected result: 2 (60 and 50 are passes). Record each variable after every pass of the loop:
| i | marks[i] | marks[i] > 50? | count |
|---|---|---|---|
| (start) | 0 | ||
| 1 | 45 | No | 0 |
| 2 | 60 | Yes | 1 |
| 3 | 50 | No | 1 |
| end | RETURN 1 |
The desk check returns 1, not 2. The mismatch appears at i = 3: a mark of exactly 50 is rejected because the test uses > instead of >=. This is a boundary fault, and it is the reason boundary values are always part of the test data. Choosing 50 as one of the desk-check values is what exposed it.
Peer checking
A peer checker reads the algorithm without the author’s assumptions. Give them a short checklist so the review is systematic:
- Can they state the purpose, inputs and outputs from the algorithm alone?
- Does every variable start with a sensible value before it is used?
- Does each loop change something that eventually ends it?
- Are boundary values (first, last, empty, exactly on a limit) handled?
- Do names, indentation and matching pairs (
IF/ENDIF,WHILE/ENDWHILE) make the structure clear?
Following the data through subroutine calls
Connections between algorithms are where data is often lost or mixed up. Draw up a small table of every call: what is passed in, what the parameter is called, and what comes back. Here is the pay example from above.
BEGIN PayEmployee (salary, bonusPercent)
tax ← CalculateTax (salary)
pay ← salary - tax + ApplyBonus (salary, bonusPercent)
RETURN pay
END PayEmployee (salary, bonusPercent)| Call | Passed in | Parameters | Comes back | Used for |
|---|---|---|---|---|
CalculateTax (salary) | salary | salary | The tax owed (one value) | Subtracted from pay |
ApplyBonus (salary, bonusPercent) | salary, bonusPercent | basePay, percent (names may differ) | The bonus amount | Added to pay |
The parameter names inside a subroutine need not match the caller’s names, but the order, number and type of the arguments must. Passing bonusPercent first and salary second would run without a crash and give a wrong answer.
Identifying Procedures and Functions
🎯 (SE-11-02)
📌 Identify procedures and functions in an algorithm
A subprogram is a named block of steps that can be called from elsewhere in an algorithm. There are two kinds, and spotting which is which tells you how the algorithm fits together.
| Procedure | Function | |
|---|---|---|
| What it does | Carries out an action | Calculates and returns a value |
| How to spot it | Called as a statement on its own line | Used inside an expression, e.g. total ← total + getPrice(item) |
| Example | printReceipt(order) | calculateGST(price) |
Functions vs Procedures
| Feature | Function | Procedure |
|---|---|---|
| Returns a value? | Yes — always returns something | No — performs an action, returns None |
| Python keyword | def + return value | def + no return (or bare return) |
| Use when | You need a computed result | You need a side effect (print, write file) |
| Example | calculate_tax(income) | print_report(data) |
Return Values
# Single return value
def circle_area(radius):
import math
return math.pi * radius ** 2
# Multiple return values (Python returns a tuple)
def min_max(numbers):
return min(numbers), max(numbers)
low, high = min_max([5, 3, 9, 1, 7])
print(low, high) # 1 9
# Function returning None (procedure style)
def greet(name):
print(f"Hello, {name}!")
# no return statement → returns None implicitly
Parameters and arguments
A parameter is the name a subprogram gives to a piece of data it expects. An argument is the actual value supplied when it is called. In calculate_gst(price), price is the parameter; in calculate_gst(80), 80 is the argument. NESA treats subroutine, module, subprogram and procedure as interchangeable words for a named group of statements; a function is the kind that returns a single value.
Worked example: identify each subprogram
Read this algorithm and decide, for each named subprogram, whether it is a procedure or a function.
BEGIN ProcessOrder (items, total)
ShowWelcome ()
gst ← CalculateGST (total)
final ← total + gst
IF items >= 10 THEN
final ← final - Discount (final)
ENDIF
PrintReceipt (items, final)
END ProcessOrder (items, total)| Subprogram | Procedure or function? | Evidence in the algorithm |
|---|---|---|
ShowWelcome | Procedure | Stands alone on its own line; nothing is assigned or used from it |
CalculateGST | Function | Its result is assigned: gst ← CalculateGST (total) |
Discount | Function | Its result is used inside an expression: final - Discount (final) |
PrintReceipt | Procedure | Called as a statement; it carries out an action (printing) |
Common mistakes with return values
A function’s value is lost unless the caller does something with it. Two errors appear again and again in student code:
def calculate_gst(price):
return price * 0.10
calculate_gst(80) # mistake: the returned 8.0 is thrown away
print(calculate_gst(80)) # 8.0: the value is used
def show_total(total):
print("Total:", total) # a procedure: prints, returns None
result = show_total(90)
print(result) # None: there is nothing useful to store8.0
Total: 90
NoneRETURN also ends the subprogram immediately. Any statement written after it in the same path never runs, which is why linear search returns from inside the loop as soon as it finds a match.
Programming Paradigms
🎯 (SE-11-02)
📌 Experiment with object-oriented programming, imperative, logic and functional programming paradigms
Object-Oriented Programming (OOP)
Data and functions are bundled together in "objects" that model real-world entities. Emphasises reusability, modularity, and maintainability. Classes define blueprints; instances are created from them.A Car class has properties (colour, speed) and methods (accelerate(), brake()); multiple car objects can be created from this template.
Imperative Programming
Programs are sequences of explicit commands (statements) that change program state step-by-step. Python, Java, and C are imperative languages. Focus on "how" to solve the problem (procedures and control flow)."Set count to 0; LOOP: add next number to count; increment loop counter; IF loop counter < 10, GO TO LOOP."
Logic Programming
Programs state facts and rules; the interpreter derives new facts through logical inference. Less common than imperative, but heavily used in artificial intelligence, constraint logic, and database query planning. Focus on "what" is true, not "how" to compute it.
% NESA requires students to "define and edit facts" and "create, edit and remove rules"
% 1. DEFINE FACTS (The fundamental truths of the knowledge base)
parent(john, mary).
parent(john, peter).
parent(mary, bob).
parent(peter, sarah).
% 2. CREATE RULES (Logic derived from facts using variables)
% "X is a grandparent of Z IF X is a parent of Y AND Y is a parent of Z"
grandparent(X, Z) :-
parent(X, Y),
parent(Y, Z).
% "A and B are siblings IF they share a parent P AND are not the same person"
sibling(A, B) :-
parent(P, A),
parent(P, B),
A \= B.
% 3. LOGIC ENGINE INFERENCE (Querying the system)
% This is how a logic paradigm dynamically solves problems without imperative loops:
% Query: grandparent(john, bob).
% Engine derivation steps:
% - Search: parent(john, Y) -> Matches Y = mary
% - Search: parent(mary, bob) -> Fact exists!
% Output: TRUE
% Query: sibling(mary, peter).
% Engine derivation steps:
% - Search: parent(P, mary) -> Matches P = john
% - Search: parent(john, peter) -> Fact exists!
% - Search: mary \= peter -> TRUE
% Output: TRUE
λ Functional Programming
Programs are built from pure functions (same input always produces same output, no side effects) and immutable data. Emphasises recursion and function composition.Example (Lisp/Scheme): Computing factorial recursively as (factorial 5) = 5 × (factorial 4), where (factorial 4) = 4 × (factorial 3), and so on down to (factorial 1) = 1, without modifying any variable.
Modern development often uses hybrid approaches — Python supports both OOP and functional styles; JavaScript supports imperative and functional paradigms. Experimenting with different paradigms strengthens problem-solving skills and helps you choose the best tool for each context.
Experiment: one problem, four ways
The syllabus asks you to experiment with the paradigms, and the best way is to solve one problem in each and compare. The problem: a canteen order has three items priced 10.00, 25.50 and 4.50 dollars. Add 10% GST to each and report the total.
Imperative: a sequence of steps that change state
prices = [10.00, 25.50, 4.50]
total = 0
for price in prices:
total = total + price * 1.10 # total is changed on every pass
print(round(total, 2))44.0Functional: expressions with no changing variables
prices = [10.00, 25.50, 4.50]
add_gst = lambda price: price * 1.10 # a pure function
print(round(sum(add_gst(p) for p in prices), 2))44.0Object-oriented: data and behaviour bundled in a class
class Cart:
def __init__(self):
self.prices = []
def add(self, price):
self.prices.append(price)
def total_with_gst(self):
return round(sum(self.prices) * 1.10, 2)
cart = Cart()
for price in [10.00, 25.50, 4.50]:
cart.add(price)
print(cart.total_with_gst())44.0Logic: facts and rules, then a query
Prolog has no loop. You state facts and a rule, then ask a question and the engine works out the answer. Here the rule says a price with GST is the price times 11, divided by 10.
price(pen, 10.00).
price(book, 25.50).
price(bag, 4.50).
with_gst(Item, Total) :- price(Item, P), Total is P * 11 / 10.
% Query: ?- with_gst(book, T).
% Answer: T = 28.05The first three give the same 44.0. The Prolog query asks about one item (25.50 with GST is 28.05) because a logic program answers questions rather than running steps. What differs between the paradigms is how you think about the problem.
| Paradigm | Core idea | You describe… | Suits | Python support |
|---|---|---|---|---|
| Imperative | Commands that change state step by step | How to do it | Scripts, control code, most beginner programs | Loops, assignment, functions |
| Object-oriented | Objects bundle data with the methods that use it | Things and what they can do | Large programs modelling real entities | Classes, inheritance |
| Logic | Facts and rules; the engine infers answers | What is true | Rule-based problems, knowledge bases | Not built in; libraries exist |
| Functional | Pure functions, no changing state | What to compute | Data transformation, easy-to-test code | Lambda, comprehensions, map |
Try this: add a fourth price with the imperative version, then with each of the others. Which was easiest to change? Which is easiest to test on its own? Write two sentences on which paradigm you would choose for a rule-based timetable checker and why.
Try it
Which programming paradigm?
Practise recognising object-oriented, imperative, logic and functional code. Enable JavaScript to use this tool. In short: objects bundle data and methods, imperative code is a list of commands, logic code states facts and rules, and functional code applies functions.
Number Systems
🎯 (SE-11-03)
📌 Investigate the use of number systems for computing purposes, including binary, decimal and hexadecimal
Computers store everything as binary, but people read and write numbers in other bases. Software engineers move between three of them.
- Binary (Base 2): Used by hardware for electrical on/off states.
- Decimal (Base 10): The standard human system for counting.
- Hexadecimal (Base 16): Efficiently represents bytes (e.g.,
#FFFFFFfor white in CSS).
| Decimal | Binary | Hexadecimal | Where you'll see it |
|---|---|---|---|
| 10 | 1010 | A | Counting, prices and marks |
| 255 | 1111 1111 | FF | The largest value in one byte; colour channels in #FF8800 |
| 192 | 1100 0000 | C0 | The first part of an IPv4 address such as 192.168.0.1 |
Converting between bases
- Binary → decimal: add the place values that hold a 1. For 1011: 8 + 0 + 2 + 1 = 11.
- Decimal → binary: divide by 2 repeatedly and read the remainders from bottom to top.
- Binary ↔ hexadecimal: split the binary into groups of four bits. 1010 1111 → A F →
AF.
Place value in three bases
Every base works the same way: each digit is worth the digit times a place value, and each place is the base times bigger than the one on its right. Decimal places are powers of 10, binary places are powers of 2 and hexadecimal places are powers of 16.
| Base | Digits used | Place values (right to left) | Example |
|---|---|---|---|
| Decimal (10) | 0 to 9 | 1, 10, 100, 1000 | 214 = 2×100 + 1×10 + 4×1 |
| Binary (2) | 0 and 1 | 1, 2, 4, 8, 16, 32, 64, 128 | 11010110 = 128 + 64 + 16 + 4 + 2 = 214 |
| Hexadecimal (16) | 0 to 9, then A to F | 1, 16, 256, 4096 | D6 = 13×16 + 6×1 = 214 |
Hexadecimal needs sixteen digits, so the letters A to F stand for the values 10 to 15:
| Hex | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Decimal | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| Binary | 0000 | 0001 | 0010 | 0011 | 0100 | 0101 | 0110 | 0111 | 1000 | 1001 | 1010 | 1011 | 1100 | 1101 | 1110 | 1111 |
Worked conversions
Decimal to binary: repeated division by 2
Convert 156. Divide by 2, record the remainder, and repeat with the quotient until it reaches 0.
| Division | Quotient | Remainder |
|---|---|---|
| 156 ÷ 2 | 78 | 0 |
| 78 ÷ 2 | 39 | 0 |
| 39 ÷ 2 | 19 | 1 |
| 19 ÷ 2 | 9 | 1 |
| 9 ÷ 2 | 4 | 1 |
| 4 ÷ 2 | 2 | 0 |
| 2 ÷ 2 | 1 | 0 |
| 1 ÷ 2 | 0 | 1 |
Read the remainders from the bottom up: 10011100. Check by adding place values: 128 + 16 + 8 + 4 = 156.
Binary to hexadecimal, and back
Split the binary number into groups of four from the right, then convert each group with the table above: 1001 1100 is 9 and C, so 156 is 9C in hexadecimal. To go back, replace each hex digit with its four bits.
Hexadecimal to decimal
Multiply each digit by its place value and add: 9C = 9 × 16 + 12 × 1 = 144 + 12 = 156. This matches the other two forms.
Decimal to hexadecimal directly
Divide by 16 instead of 2: 156 ÷ 16 = 9 remainder 12, and 12 is C. Reading the remainders from the bottom gives 9C.
Checking conversions in Python
number = 156
print(bin(number)) # 0b10011100
print(hex(number)) # 0x9c
print(f"{number:08b}") # 10011100 (8 bits, padded with zeros)
print(f"{number:X}") # 9C
print(int("10011100", 2)) # 156
print(int("9C", 16)) # 1560b10011100
0x9c
10011100
9C
156
156Where each base is used
- Binary is how hardware stores everything. A bit is one binary digit; a byte is eight bits and can hold 28 = 256 different values (0 to 255).
- Hexadecimal shortens binary for people: colour codes such as
#FF8800(red FF = 255, green 88 = 136, blue 00 = 0), memory addresses and MAC addresses. - Decimal is for people: prices, marks and counts.
| Number of bits | Different values | Unsigned range |
|---|---|---|
| 4 | 16 | 0 to 15 |
| 8 | 256 | 0 to 255 |
| 16 | 65 536 | 0 to 65 535 |
| 32 | 4 294 967 296 | 0 to 4 294 967 295 |
Quick self-check: four conversions
- Convert decimal 45 to binary.
- Convert hexadecimal B7 to decimal.
- Convert hexadecimal 2F to binary.
- What is the largest value an unsigned 8-bit number can hold, in decimal and in hexadecimal?
Answers. (1) 45 = 32 + 8 + 4 + 1 = 101101, or 00101101 in 8 bits. (2) B7 = 11 × 16 + 7 = 183. (3) 2 = 0010 and F = 1111, so 00101111. (4) 255, which is FF.
9C in hexadecimal). Pad binary answers to the number of bits asked for. For investigate, also say why a base is used: binary because hardware has two states, hexadecimal because one digit stands for exactly four bits.Two's Complement
🎯 (SE-11-03)
📌 Represent integers using two’s complement
Two's Complement is the standard method for representing signed (negative) integers in binary. To represent a negative number, the bits of the positive equivalent are inverted (0 to 1 and vice-versa) and 1 is added to the result. This system is preferred as it allows for simple addition/subtraction circuits and eliminates the 'double zero' (+0 and -0) problem used in older systems.
Worked Example — Representing -5 in 8-bit Two's Complement:- Start with the positive number: 5 =
00000101 - Invert all bits (One's Complement):
11111010(each 0 becomes 1, each 1 becomes 0) - Add 1 to the result:
11111010 + 1 = 11111011 - Result: -5 in 8-bit Two's Complement =
11111011
Verification: If we add 5 and -5 in binary: 00000101 + 11111011 = 100000000 (the overflow bit carries out, leaving 00000000 = 0 ✓)
# Representing integers in different bases
number = 255
print(bin(number)) # 0b11111111 (Binary)
print(hex(number)) # 0xff (Hexadecimal)
# Binary to Decimal for hardware addresses
print(int("1010", 2)) # 10
An approachable explainer on how computers store everything as binary — perfect background for understanding number systems and two's complement.
Why two’s complement?
A computer needs a way to store negative whole numbers using only 0s and 1s. The simplest idea is to use the left-hand bit as a sign, but that gives two zeros (+0 and −0) and makes subtraction awkward. In two’s complement there is one zero, and the same adding circuit handles positive and negative numbers.
Range of an n-bit number
The left-hand bit is the sign bit: 0 for zero and positive numbers, 1 for negative numbers. With n bits the range is −2n−1 to 2n−1 − 1. There is one more negative value than positive.
| Bits | Smallest | Largest |
|---|---|---|
| 4 | −8 | 7 |
| 8 | −128 | 127 |
| 16 | −32 768 | 32 767 |
| 32 | −2 147 483 648 | 2 147 483 647 |
More worked examples (8 bits)
| Value | Positive form | Invert | Add 1 | Result |
|---|---|---|---|---|
| −5 | 00000101 | 11111010 | 11111011 | 11111011 |
| −1 | 00000001 | 11111110 | 11111111 | 11111111 |
| −20 | 00010100 | 11101011 | 11101100 | 11101100 |
| −128 | 10000000 (no +128 in 8 bits) | 01111111 | 10000000 | 10000000 |
Reading a negative pattern
To find the value of 11110110: the sign bit is 1, so it is negative. Invert the bits (00001001), add 1 (00001010 = 10) and put a minus sign in front: −10. You can check with signed place values: the sign bit is worth −128, so −128 + 64 + 32 + 16 + 4 + 2 = −10.
Subtraction becomes addition
To calculate 7 − 5, add 7 and −5: 00000111 + 11111011 = 1 00000010. The carry out of the eighth bit is discarded, leaving 00000010, which is 2.
Overflow
If a result does not fit in the available bits, it wraps round. In 8 bits, 01111111 (127) plus 1 is 10000000, which is −128. This is overflow, and it is why the Year 2038 problem in the section introduction matters: a 32-bit signed count of seconds runs out of positive values in January 2038.
print(f"{-5 & 0xFF:08b}") # 11111011 -5 as 8 bits
print(int("11111011", 2) - 256) # -5 read 8 bits back as signed
print(f"{-20 & 0xFF:08b}") # 11101100
print(int("11110110", 2) - 256) # -1011111011
-5
11101100
-10In Python, & 0xFF keeps only the lowest eight bits, which is what an 8-bit store would keep. Subtracting 256 (28) from a pattern whose sign bit is 1 gives its signed value.
Explore it: number base and two’s complement workbench
Use the workbench to check your own conversions. Type a number in any base, or click the bits, and it shows the working. Try −5, then 127 with the Add 1 button to see overflow, then open the self-check at the bottom.
This workbench needs JavaScript. Without it, use the worked examples above: for a negative number write the positive value in binary, invert every bit, then add 1; to read a pattern whose left-hand bit is 1, invert, add 1 and put a minus sign in front.
Quick self-check: two’s complement
- Write −7 as a 4-bit two’s complement number.
- What decimal number is
10011010in 8-bit two’s complement? - What is the range of a 6-bit two’s complement number?
Answers. (1) 7 = 0111; invert to 1000; add 1 to get 1001. (2) The sign bit is 1: invert to 01100101, add 1 to get 01100110 = 102, so the value is −102. (3) −25 to 25 − 1, which is −32 to 31.
Standard Data Types
🎯 (SE-11-03, SE-11-04)
📌 Investigate standard data types
- char (character) and string
- Boolean
- real
- single precision floating point
- integer
- date and time
A data type tells the computer what kind of value a variable holds, how much memory to set aside, and which operations make sense. Choosing the right type prevents errors and saves memory.
| Data Type | Investigated Description | Python Example |
|---|---|---|
| Char / String | Char represents one character; String is a sequence of characters. | "HSC" |
| Boolean | Logical values used for binary decisions. | is_valid = True |
| Integer | Whole numbers stored using binary/Two's Complement. | age = 17 |
| Real (Float) | Decimal numbers. Single Precision refers to 32-bit floats. | gpa = 3.95 |
| Date / Time | Often stored as a count of seconds from an epoch such as 1 January 1970. NESA specifies that a date and time is stored as 32 bits (4 bytes). | import datetime |
Choosing a data type
- Use an integer for things you count (students, clicks). Use a real for things you measure (temperature, price).
- A single precision floating point number uses 32 bits and keeps about 7 significant digits, so it can round large or very precise values. That's why money is often stored as whole cents.
- Store phone numbers and postcodes as strings. You never do arithmetic on them, and a leading zero would disappear from an integer.
- Store dates with a date/time type rather than text, so you can sort them and calculate differences.
price = 0.1 + 0.2 as a float and shows $0.30000000000000004. Storing the price as an integer number of cents (30) avoids the rounding error.Try it
Single precision floating point explorer
See the 32 stored bits of a decimal number and the value that is really kept. Enable JavaScript to use this tool. In short: a single precision float has 1 sign bit, 8 exponent bits and 23 fraction bits, and most decimal fractions such as 0.1 cannot be stored exactly.
The six standard types in detail
char (character) and string
A char holds one character: a letter, digit, space or symbol. Internally it is stored as a number, its code in ASCII or Unicode. A string is a sequence of characters. Python has no separate char type; a char is a string of length 1.
print(ord("A")) # 65 the code for A
print(chr(97)) # a the character with code 97
word = "HSC"
print(word[0], word[1:], len(word)) # H SC 365
a
H SC 3Boolean
A Boolean holds one of two values, True or False. Every comparison produces one, and every IF and WHILE test relies on one. It needs only a single bit of storage. Name Boolean variables as questions: is_valid, has_discount.
mark = 50
passed = mark >= 50
print(passed, type(passed).__name__) # True boolTrue boolinteger
An integer is a whole number with no fractional part, positive or negative, stored in binary (two’s complement for negatives). Use it for anything you count. In many languages an integer has a fixed size; a 16-bit signed integer holds −32 768 to 32 767 and a 32-bit one about ±2 billion. Python integers grow as needed, so overflow is a concern in other languages and in hardware.
real
A real number has a fractional part, such as a measurement or a rate. It is stored in floating point form, which stores most values only approximately. That leads to a classic surprise:
print(0.1 + 0.2) # 0.30000000000000004
print(0.1 + 0.2 == 0.3) # False
print(abs((0.1 + 0.2) - 0.3) < 1e-9) # True: compare with a small tolerance
print(round(0.1 + 0.2, 2)) # 0.30.30000000000000004
False
True
0.3Never test two real numbers for exact equality. Compare them within a small tolerance, or store money as whole cents.
single precision floating point
Single precision uses 32 bits (4 bytes) for a real number: 1 sign bit, 8 bits for the exponent (the scale) and 23 bits for the fraction (the digits). That gives about 7 significant decimal digits. Values beyond that precision are rounded, as the next example shows.
import struct
def as_single(value):
"""Store value as a 32-bit float and read it back."""
return struct.unpack("f", struct.pack("f", value))[0]
print(as_single(0.1)) # 0.10000000149011612
print(as_single(16777217)) # 16777216.00.10000000149011612
16777216.0The first line shows that 0.1 cannot be stored exactly. The second shows that after 16 777 216 (224) single precision can no longer represent every whole number, so 16 777 217 is stored as 16 777 216. Double precision (64 bits) is the default real type in Python and gives about 15 to 16 digits.
date and time
A date and time type stores a moment as one number, so dates can be sorted and subtracted. NESA specifies that it is always stored as 32 bits (4 bytes) and displayed in whatever format the program chooses, such as DD/MM/YYYY. A 32-bit count of seconds from 1 January 1970 runs out in January 2038, which is the Year 2038 problem.
from datetime import date
enrolled = date(2026, 3, 14)
print(enrolled.strftime("%d/%m/%Y")) # 14/03/2026
deadline = date(2026, 10, 12)
print((deadline - enrolled).days) # 212 days between the two dates14/03/2026
212Storing a date as text such as "14/03/2026" would sort in the wrong order and make date arithmetic hard, which is why a real date type is used.
| Data | Best type | Reason |
|---|---|---|
| Student ID such as 10482 | Integer, or string if leading zeros matter | Whole number; you never do arithmetic on it |
| Price in a canteen | Integer number of cents | Avoids floating point rounding |
| Temperature from a sensor | Real (single precision is often enough) | Has a fractional part; a few digits are enough |
| Is the user logged in? | Boolean | Only two states |
| Enrolment date | Date and time | Sorting and calculating differences |
| First initial | Char (string of length 1) | A single character |
Data Dictionaries
🎯 (SE-11-03, SE-11-04)
📌 Create data dictionaries as a tool to describe data and data types, structure data, and record relationships
A data dictionary is a table that gives a comprehensive description of each variable stored or referred to in a system, before any code is written. NESA’s Course Specifications list what it commonly includes: the variable name, data type, format for display, size in bytes, size for display, the purpose of the variable, a relevant example and any validation rules. Everyone on the team then works from the same definitions.
Table
Data dictionary: a student management system
Every variable is described before any code is written, using the columns in NESA’s Course Specifications.
| Variable | Data type | Format for display | Size in bytes | Size for display | Description | Example | Validation |
|---|---|---|---|---|---|---|---|
| StudentId | Integer | NNNNN | 4 | 5 | Primary key; identifies a student | 10482 | 10000 to 99999; unique |
| FirstName | String | Xxxx… | 20 | 20 | Student’s given name | Priya | Not empty; letters only; first letter a capital |
| YearLevel | Integer | NN | 1 | 2 | Current year of school | 11 | Must be 11 or 12 |
| EnrolDate | Date and Time | DD/MM/YYYY | 4 | 10 | Date the student enrolled | 05/02/2025 | Valid date, not in the future |
| FeesOwing | Floating Point | $NNN.NN | 4 | 7 | Fees still to be paid | $45.50 | Decimal from 0 to 999.99 |
| HasConsent | Boolean | X | 1 bit | 1 | Photo permission | Y | Y or N |
| Subjects | Array (string) | 20 × subjects | N/A | Subjects studied | Software Engineering | From a drop-down list |
What to notice
- Each row describes one variable across eight columns: name, type, display format, storage size, display size, purpose, example and validation.
- Size in bytes is what is stored; size for display is the number of characters shown, including any decimal point or dollar sign.
- Validation rules become the checks your code performs on input.
Try thisWrite a data dictionary for a library book: ISBN, title, author, copies available and last borrowed date. Include all eight columns.
Recording relationships
When data is split across tables, the dictionary also records how they link. For example, Enrolment.StudentId must match an existing Student.StudentId, so every enrolment belongs to a real student. The primary key (Student.StudentId) uniquely identifies each row; the matching column in the other table is a foreign key. One student can have many enrolments, so this is a one-to-many relationship. Note the key and the link in the Description or Validation column.
Describing data and data types
Each column answers a different question a programmer will ask when writing or testing code:
| Column | Question it answers | Tip for writing it |
|---|---|---|
| Variable | What is it called in the code? | Use one consistent style, such as StudentId, and the same name in the program |
| Data type | What kind of value is it? | Choose from the standard types: string, integer, floating point, Boolean, date and time |
| Format for display | How does it look on screen? | Use N for a digit and X for a letter, such as XXNNN |
| Size in bytes | How much storage does it need? | Integer 4, single precision 4, date and time 4, Boolean 1 bit |
| Size for display | How many characters are shown? | Count decimal points, currency symbols and slashes as well |
| Description | What is it for? | State the purpose in the user’s terms; note if it is a key |
| Example | What does a real value look like? | Use a realistic value that passes validation |
| Validation | What values are allowed? | State a testable rule: a range, a format, a list, unique, not empty |
Structuring data
A data dictionary also records how variables are grouped. The variables above form one record: a student. Several students form an array of records. The dictionary can list the record once, then note that the system stores an array of them. In Python:
students = [
{"StudentId": 10482, "FirstName": "Priya", "YearLevel": 11},
{"StudentId": 10517, "FirstName": "Marcus", "YearLevel": 12},
]
for student in students:
print(student["StudentId"], student["FirstName"])10482 Priya
10517 MarcusTurning validation rules into code
Every validation entry in the dictionary should become a check in the program, and a test case. Here are two rules from the table:
def valid_year_level(value):
"""YearLevel: integer, must be 11 or 12."""
return isinstance(value, int) and value in (11, 12)
def valid_student_id(value, existing_ids):
"""StudentId: whole number 10000 to 99999, unique."""
return isinstance(value, int) and 10000 <= value <= 99999 and value not in existing_ids
print(valid_year_level(11), valid_year_level(10), valid_year_level("11"))
print(valid_student_id(10482, [10517]), valid_student_id(10517, [10517]), valid_student_id(9999, []))True False False
True False FalseWorked example: creating a data dictionary
Follow these steps for any system. For the canteen order calculator: (1) list every value the algorithm needs from the requirements; (2) name each in one style; (3) choose a data type; (4) decide the display format and sizes; (5) write a description and a realistic example; (6) add a validation rule you can test.
| Variable | Data type | Format for display | Size in bytes | Size for display | Description | Example | Validation |
|---|---|---|---|---|---|---|---|
| PriceCents | Integer | NNNN | 4 | 4 | Price of one item in cents | 450 | Whole number 1 to 9999 |
| Quantity | Integer | NN | 2 | 2 | How many of that item | 6 | Whole number 1 to 99 |
| ItemCount | Integer | NNN | 4 | 3 | Running count of items in the order | 10 | Whole number 0 or more |
| OrderTotal | Integer | $NNN.NN | 4 | 7 | Order total in cents, shown in dollars | $35.10 | Whole number 0 or more |
Arrays, Records, Trees and Sequential Files
🎯 (SE-11-03, SE-11-04)
📌 Use data structures of arrays, records, trees and sequential files
Arrays (Single and Multidimensional)
Fixed-size collections of elements of the same type. Single-dimensional arrays store a list; multidimensional arrays form grids or tables.scores = [45, 67, 89] (1D), or board = [[1,2,3],[4,5,6],[7,8,9]] (2D chessboard).
# Single dimension
scores = [45, 67, 89]
# Multidimensional
board = [[0 for _ in range(8)] for _ in range(8)]
Records
Structured collections that group related data fields of different types under one name. A record is like a row in a database table or an object with named fields.A Student record contains {Name (string), StudentID (integer), GPA (float), EnrolmentDate (date)}.
# In Python, using a class or dictionary
student = {
"name": "Alice",
"id": 12345,
"gpa": 3.95
}
Trees
Hierarchical structures with a root node and parent-child relationships. Used for representing file systems, organizational charts, and search optimisation.A file system tree where C:\ is root, containing Users, Program Files, etc.
Sequential Files
Data stored in order in a file, accessed sequentially from beginning to end. Unlike databases, sequential access is required (cannot jump directly to record 100).A CSV file of sales records is read line-by-line in order; to find a specific customer, the entire file must be scanned.
Stacks (LIFO)
Last-In-First-Out data structure. Last element added is the first removed. Used for function call stacks, undo features, expression evaluation.The 'Undo' feature in Word — each action is pushed; undoing pops the most recent action.
history = []; history.append("Action"); history.pop()
Hash Tables
Key-value mappings for instant lookup. Hash functions convert keys to array indices, enabling O(1) average-case access time.A username-to-profile dictionary where lookup("alice") instantly returns her profile without scanning.
users = {"alice": {"age": 17, "year": 11}}
Shows how data is organised using arrays, stacks, queues, and trees — and why choosing the right structure matters.
Using each structure: worked examples
Arrays
An array is read by position. Positions start at 0. A two-dimensional array needs a row and a column. (Python lists are more flexible than the fixed-size arrays of other languages, but are used the same way.)
scores = [45, 67, 89]
print(scores[0], scores[2], len(scores)) # first, last, how many
board = [[1, 2, 3],
[4, 5, 6],
[7, 8, 9]]
print(board[1][2]) # row 1, column 2
for row in board:
print(row)45 89 3
6
[1, 2, 3]
[4, 5, 6]
[7, 8, 9]Records
A record groups fields of different types about one thing. A collection of records is a list of them.
student = {"name": "Alice", "id": 12345, "gpa": 3.95}
print(student["name"], student["id"])
student["gpa"] = 4.0
print(student)Alice 12345
{'name': 'Alice', 'id': 12345, 'gpa': 4.0}Trees
A tree is naturally processed by a function that handles one node, then calls itself on each child. This prints a folder hierarchy with indentation for depth:
folders = {"Documents": {"Reports": {}, "Photos": {"2026": {}}}, "Music": {}}
def show(tree, depth=0):
for name, children in tree.items():
print(" " * depth + name)
show(children, depth + 1)
show(folders)Documents
Reports
Photos
2026
MusicSequential files
A sequential file is written and read from the start, one record at a time. To find one record you scan from the top until you reach it.
with open("scores.txt", "w") as file: # write mode replaces the file
for name, mark in [("Ava", 72), ("Ben", 85), ("Cai", 64)]:
file.write(f"{name},{mark}\n")
with open("scores.txt") as file: # read it back in order
for line in file:
name, mark = line.strip().split(",")
print(name, mark)Ava 72
Ben 85
Cai 64The file scores.txt now holds three lines, Ava,72, Ben,85 and Cai,64. Using with closes the file automatically, even if an error occurs.
Stacks and hash tables
A stack only lets you add and remove at the top. A hash table uses a hash function to turn a key into a slot number, so it can jump straight to a value. Two keys can land in the same slot; that is a collision, and the table needs a plan for it (implemented in the next part). A toy hash function that adds the character codes and takes the remainder after dividing by 7 shows this:
def toy_hash(key, size=7):
return sum(ord(ch) for ch in key) % size
for key in ["Ava", "Ben", "Cai", "Dev"]:
print(key, "goes to slot", toy_hash(key))Ava goes to slot 0
Ben goes to slot 4
Cai goes to slot 3
Dev goes to slot 0“Ava” and “Dev” collide in slot 0.
Choosing a structure
| Structure | Access | Good for | Weakness |
|---|---|---|---|
| Array | By position (index) | Lists and grids of the same type | Adding or removing in the middle is awkward |
| Record | By field name | One thing with mixed-type fields | One record only; combine records in an array |
| Tree | From the root down | Hierarchies such as folders, and fast searching | More complex to build and traverse |
| Sequential file | In order, start to end | Storing data after the program ends | Finding one record means scanning |
| Stack | Top only (last in, first out) | Undo, back button, function calls | No access to items below the top |
| Hash table | By key | Fast lookup by ID or name | Unordered; collisions must be handled |
Developing a Software Solution
🎯 (SE-11-06)
📌 Apply skills in computational thinking and programming to develop a software solution
- converting an algorithm into code
- using control structures
- using data structures
- using standard modules
- creating relevant subprograms that incorporate parameter passing
Converting Algorithm to Code
Translating pseudocode or flowcharts into actual programming language syntax. Each algorithm step becomes a statement; pseudocode's IF becomes the language's if keyword; loops translate directly.
Using Control Structures
Implementing sequence (statements in order), selection (if/else, switch), and iteration (for, while loops) to direct program flow based on conditions and data.
Using Data Structures
Selecting and implementing appropriate data structures (arrays, records, trees, hash tables) to organise data so algorithms can access it efficiently.
Using Standard Modules
Leveraging built-in libraries and functions (e.g., Python's math module, datetime module, string methods) instead of reinventing functionality. Standard modules are tested, optimised, and consistent across projects.
Creating Subprograms with Parameter Passing
Writing functions and procedures that accept parameters (inputs), perform a specific task, and return results. Parameters can be passed by value (copy) or by reference (address), allowing functions to modify caller's data when needed.FUNCTION calculateBonus(salary, percent) takes two parameters, calculates the bonus, and returns the result.
Parameter Passing
Pass by value: A copy of the variable is sent. The original variable in the caller is unchanged, even if the function modifies the parameter.
def double(n):
n = n * 2 # modifies the local copy only
return n
score = 50
result = double(score)
print(score) # still 50 — original unchanged
print(result) # 100
Pass by reference (mutable objects): Python passes a reference to the object. Mutable types (lists, dicts) can be modified inside the function — the caller sees the change.
def add_score(scores, new_score):
scores.append(new_score) # modifies the original list
results = [80, 75, 90]
add_score(results, 95)
print(results) # [80, 75, 90, 95] — list WAS modified
scores = []) only re-points the local name and never changes the caller's list.
Variable Scope
Scope determines which parts of your program can access a variable. Python uses the LEGB rule to look up names:
LEGB Lookup Order ───────────────────────────────────────────── L Local — variables defined inside the current function E Enclosing — variables in any enclosing (outer) function G Global — variables defined at module level B Built-in — Python's built-in names (print, len, range…) ───────────────────────────────────────────── Python searches L → E → G → B until the name is found.
x = 10 # Global scope
def show():
x = 99 # Local scope — shadows the global x
print(x) # 99
show()
print(x) # 10 — global unchanged
# Using global keyword (avoid when possible)
counter = 0
def increment():
global counter
counter += 1
increment()
print(counter) # 1
global variables. Global state makes programs harder to debug and test. If multiple functions need the same data, pass it as a parameter.
Worked example: converting NESA pseudocode to Python
Converting is a line-by-line translation, once you know the equivalent of each NESA structure in Python.
| NESA pseudocode | Python | Note |
|---|---|---|
total ← total + price | total = total + price | Assignment uses = in Python |
IF … THEN … ELSE … ENDIF | if … : … else: … | Indentation replaces ENDIF |
| Nested IF (an IF inside each ELSE) | if … : … elif … : … else: … | elif is the flattened form of the nested structure |
CASEWHERE … OTHERWISE … END CASE | match … : case … : case _ : | match needs Python 3.10 or later; if/elif also works |
WHILE condition … ENDWHILE | while condition: | Pre-test loop |
REPEAT … UNTIL condition | while True: … if condition: break | Python has no post-test loop, so the test is at the end |
FOR i = 1 TO 5 … NEXT i | for i in range(1, 6): | range stops before its second value |
BEGIN name (a, b) … RETURN x | def name(a, b): … return x | Parameters are passed in the brackets |
menu_choice = 2
match menu_choice: # CASEWHERE menuChoice evaluates to
case 1:
print("New game")
case 2:
print("Load game")
case 3:
print("High scores")
case _: # OTHERWISE
print("Invalid choice")Load gameWorked example: a marks report using all five skills
This short program converts an algorithm into code and uses control structures, a data structure (a list), a standard module (statistics) and subprograms with parameters. The algorithm is written first.
BEGIN MarksReport
marks ← [72, 85, 64, 90, 58]
OUTPUT "Average: ", Average (marks)
OUTPUT "Highest: ", Highest (marks)
OUTPUT "Grade of highest: ", GradeFor (Highest (marks))
END MarksReport
BEGIN GradeFor (mark)
IF mark >= 85 THEN
RETURN "High Distinction"
ELSE
IF mark >= 75 THEN
RETURN "Distinction"
ELSE
IF mark >= 50 THEN
RETURN "Pass"
ELSE
RETURN "Fail"
ENDIF
ENDIF
ENDIF
END GradeFor (mark)import statistics # standard module: tested code we do not have to write
def average(marks):
return statistics.mean(marks)
def highest(marks):
best = marks[0]
for mark in marks: # iteration over a list (data structure)
if mark > best: # selection
best = mark
return best
def grade_for(mark):
if mark >= 85:
return "High Distinction"
elif mark >= 75:
return "Distinction"
elif mark >= 50:
return "Pass"
else:
return "Fail"
marks = [72, 85, 64, 90, 58]
print("Average:", round(average(marks), 1))
print("Highest:", highest(marks))
print("Grade of highest:", grade_for(highest(marks)))Average: 73.8
Highest: 90
Grade of highest: High DistinctionStandard modules save time and are already tested. Three you will use often: math (for example math.sqrt(144) gives 12.0), statistics (mean, median) and datetime (dates and times). Import a module once at the top, then call its functions.
def reset_scores(scores):
scores = [] # re-points the local name only
def clear_scores(scores):
scores.clear() # changes the caller's list in place
results = [80, 75, 90]
reset_scores(results)
print(results) # [80, 75, 90] unchanged
clear_scores(results)
print(results) # [] changed[80, 75, 90]
[]range(1, 6) gives 1 to 5), that every branch of a nested decision returns or assigns a value, and that arguments are passed in the order the parameters expect. Where a question shows a subroutine that changes a list, state whether the caller sees the change and why.Implementing Data Structures for Storage
🎯 (SE-11-02, SE-11-04)
📌 Implement data structures that support data storage
- single and multidimensional arrays
- lists
- trees
- stacks
- hash tables
Once you know which data your program needs, choose a structure that stores it well. Each structure below is built into Python or easy to make from a list or dictionary.
| Structure | What it stores | Python example |
|---|---|---|
| Single-dimensional array | A fixed row of values of one type, found by index | marks = [72, 85, 64] |
| Multidimensional array | A grid of values, found by row and column | seats[2][5] is row 2, seat 5 |
| List | An ordered collection that can grow and shrink | queue.append("Priya") |
| Tree | Nodes in a hierarchy, each with one parent | A folder structure, or a binary search tree of names |
| Stack | Last in, first out (LIFO) | undo.append(action) then undo.pop() |
| Hash table | Key–value pairs with very fast lookup | phone = {"Ava": "0412 345 678"} |
history = [] # the stack
def do(action):
history.append(action) # push
def undo():
if history:
return history.pop() # pop: last action comes off first
do("type A"); do("type B")
print(undo()) # type B
Single and multidimensional arrays
Create a grid with a loop, not by repeating a row: [["free"] * 4] * 3 makes three references to the same row, so changing one seat changes them all.
plan = [["free"] * 4 for _ in range(3)] # 3 rows, 4 seats, each row separate
plan[1][2] = "Ava" # row 1, seat 2
for row in plan:
print(row)['free', 'free', 'free', 'free']
['free', 'free', 'Ava', 'free']
['free', 'free', 'free', 'free']Lists
A list grows and shrinks, which makes it the usual choice when the number of items is not known in advance.
queue = ["Priya", "Ben"]
queue.append("Cai") # add to the end
queue.insert(0, "Ava") # add at the front
queue.remove("Ben") # remove by value
print(queue, len(queue))
queue.sort()
print(queue)['Ava', 'Priya', 'Cai'] 3
['Ava', 'Cai', 'Priya']Trees
A binary search tree keeps smaller values to the left and larger to the right, so an in-order walk visits values in sorted order.
class Node:
def __init__(self, value):
self.value = value
self.left = None
self.right = None
def insert(node, value):
if node is None:
return Node(value) # base case: an empty spot
if value < node.value:
node.left = insert(node.left, value)
else:
node.right = insert(node.right, value)
return node
def in_order(node):
if node is not None:
in_order(node.left)
print(node.value, end=" ")
in_order(node.right)
root = None
for name in ["Kim", "Ben", "Zoe", "Ava", "Leo"]:
root = insert(root, name)
in_order(root)Ava Ben Kim Leo ZoeStacks
The undo example above pushes and pops. Stacks also check that brackets match: push each opening bracket and pop when a closing one appears.
def balanced(text):
stack = []
pairs = {")": "(", "]": "["}
for ch in text:
if ch in "([":
stack.append(ch) # push
elif ch in ")]":
if not stack or stack.pop() != pairs[ch]: # pop and compare
return False
return not stack # nothing left open
print(balanced("(a[1] + 2)"), balanced("(a[1)]"))True FalseHash tables
Python’s dict is a hash table. Building a small one shows what happens inside: a hash function picks a slot, and each slot holds a short list so that colliding keys can share it.
class HashTable:
def __init__(self, size=7):
self.slots = [[] for _ in range(size)]
def _index(self, key):
return sum(ord(c) for c in key) % len(self.slots)
def put(self, key, value):
bucket = self.slots[self._index(key)]
for pair in bucket:
if pair[0] == key: # key already stored: replace
pair[1] = value
return
bucket.append([key, value])
def get(self, key):
for k, v in self.slots[self._index(key)]:
if k == key:
return v
return None
table = HashTable()
table.put("Ava", 72)
table.put("Dev", 55) # collides with Ava in slot 0
table.put("Ben", 85)
print(table.get("Dev"), table.get("Ava"), table.get("Zed"))
print(table.slots[0])55 72 None
[['Ava', 72], ['Dev', 55]]append/pop for a stack, index for an array, key for a hash table) and what happens at the edges: popping an empty stack, an index past the end, a missing key. State which structure you chose and why.Try it
Data structure playground
Use a stack, a hash table and a binary search tree. Enable JavaScript to use this tool. In short: a stack is last in, first out; a hash table turns a key into a slot number; and a binary search tree sends smaller values left and larger values right.
Waterfall and Agile
🎯 (SE-11-02)
📌 Compare the execution of the Waterfall and Agile project management models as applied to software development
Waterfall and Agile both take software from an idea to a finished product, but they run the project very differently. Waterfall completes each stage once, in order. Agile repeats short cycles and delivers working software along the way.
| Model | Analysis / Comparison | Example Case |
|---|---|---|
| Waterfall | A linear, rigid sequence of phases. Best when requirements are fixed and changes are unlikely. Documentation is exhaustive up-front. | Building a flight-control system for NASA where failure is not an option. |
| Agile | Iterative and flexible. Work is done in 'sprints' with continuous user feedback. Better for projects where requirements evolve. | Developing a new social media app where user trends change weekly. |
| Execution | Waterfall | Agile |
|---|---|---|
| Planning | All up front | A little before each sprint |
| Client involvement | At the start and the end | Throughout, at every review |
| Response to change | Slow and costly | Expected and welcomed |
| Working software | Near the end | After the first sprint |
| Documentation | Detailed at every stage | Just enough to support the work |
How each model runs
Waterfall runs the development steps as a sequence of phases, each finished and signed off before the next begins: requirements, design, development, testing, installation, maintenance. It relies on getting the requirements right at the start, because going back is costly. Agile repeats the steps in short cycles. In a common form, work is taken from a prioritised backlog into a fixed-length sprint (often one to four weeks), the team meets briefly each day, and at the end shows working software to the client, reviews what happened and plans the next sprint. The Agile Manifesto values working software, customer collaboration and responding to change over comprehensive documentation, contracts and following a plan, though it still values the items on the right.
Animated diagram
Waterfall and Agile: when the client gets working software
The same project run both ways. Choose a tab, then step through the twelve weeks.
What to notice
- Waterfall delivers everything at the end; the client sees nothing usable until then.
- Agile delivers a usable part every sprint, so the client can react early.
Choose Waterfall or Agile with the tabs. Step with ◀ and ▶ (or the arrow keys), play or pause with Space.
Try thisThe client changes a requirement in week 5. Which model absorbs that more easily, and why?
The canteen program under each model
| Question | Waterfall | Agile |
|---|---|---|
| How is the work organised? | Requirements for the whole program written and signed off; then design; then code; then test | Sprint 1: enter items and totals. Sprint 2: discount. Sprint 3: input checks and receipts |
| When does the canteen manager first see it? | When testing is finished | At the end of sprint 1, working but basic |
| The manager asks for a teacher discount | A change request: revise the requirements and design, then rework code and tests | Add it to the backlog and choose which sprint it goes in |
| Main risk | The finished program does not match what is now needed | Without care, features keep growing (scope creep) and documentation is thin |
Choosing between them
- Waterfall suits projects with fixed, well-understood requirements and heavy documentation or approval needs, such as safety-critical control software or a system that must meet a legal specification.
- Agile suits projects where requirements will change or be discovered as the team learns, and where a client can be involved often, such as a new app.
- Many teams mix them: fixed planning up front and iterative delivery inside it. You meet this hybrid, called WAgile, in Year 12.
Testing and Evaluating Solutions
🎯 (SE-11-06, SE-11-07)
📌 Test and evaluate solutions, considering key aspects including functionality, performance, readability of code, quality of documentation
Testing asks "does it work?" Evaluating asks "is it good?" A solution can pass every test and still be slow, hard to read or poorly documented, so judge it against all four criteria.
Evaluation Criteria
Software solutions are evaluated on multiple dimensions:
- Functionality: Does the program produce correct outputs for all inputs?
- Performance: Does it run efficiently (time complexity, memory usage)?
- Readability: Is the code clear, well-structured, and easy to understand?
- Documentation Quality: Are functions documented? Are complex sections explained?
Python Naming Conventions
student_name, calculate_average(), max_score.MAX_SCORE = 100, PI = 3.14159, DATABASE_URL.BankAccount, StudentRecord, DataProcessor._validate().Descriptive Naming — Good vs Bad
| Bad Name | Good Name | Why It's Better |
|---|---|---|
x | student_age | Describes what the variable stores |
n | num_students | Clear abbreviation with context |
data | exam_scores | Specific about what data it holds |
flag | is_logged_in | Boolean names start with is/has/can |
temp | temperature_celsius | Includes units for clarity |
calc() | calculate_gst(price) | Name tells you what it calculates |
1. Declare variables close to where they are first used — not all at the top.
2. Initialise variables to sensible defaults (e.g.
total = 0, names = []).3. Use constants for magic numbers — replace
score * 0.9 with score * PASS_THRESHOLD.4. One variable, one purpose — don't reuse a variable for different things in the same function.
Applying the four criteria
To evaluate a solution, look for evidence against each criterion. The two functions below do the same job. Judge them.
Version A
def f(a, b):
return a * b * 0.1Version B
GST_RATE = 0.10
def gst_amount(price, quantity):
"""Return the GST in dollars for one line item."""
return price * quantity * GST_RATE| Criterion | Version A | Version B |
|---|---|---|
| Functionality | Correct output for GST, but no way to tell that from the code | Same output, and the purpose is stated |
| Performance | One multiplication chain: fast | Same cost: fast |
| Readability | f, a, b and a bare 0.1 hide the meaning | Descriptive names and a named constant explain themselves |
| Quality of documentation | None | A docstring states the purpose and units |
Both are equally correct and fast, but B is far easier to check, change and hand over. That is what evaluating adds beyond testing.
Testing methods you should know
NESA’s Course Specifications list six methods for testing a software solution:
| Method | What it is | Example |
|---|---|---|
| Functional testing | Checking each function of the software against the specification | Entering 10 items and confirming the 10% discount is applied |
| Acceptance testing | The client or users confirm the software meets their needs before accepting it | The canteen manager works through a real lunch order |
| Live data | Testing with real data from actual operation | Using last week’s real orders |
| Simulated data | Testing with made-up data that imitates real data | Generating 500 pretend orders, including extreme values |
| Beta testing | Release to a limited group of real users outside the development team | Two canteens use version 0.9 for a week and report problems |
| Volume testing | Testing with a large amount of data or many users | Feeding in 100 000 orders to see if it slows or fails |
Evaluating performance without a stopwatch
Counting the steps an algorithm takes is a fair way to compare designs, because it does not depend on the computer. Count comparisons for linear and binary search on a sorted list of 1000 numbers, looking for the last one:
def linear_comparisons(items, target):
count = 0
for item in items:
count += 1
if item == target:
break
return count
def binary_comparisons(items, target):
low, high, count = 0, len(items) - 1, 0
while low <= high:
mid = (low + high) // 2
count += 1
if items[mid] == target:
break
elif items[mid] < target:
low = mid + 1
else:
high = mid - 1
return count
data = list(range(1, 1001))
print(linear_comparisons(data, 1000), binary_comparisons(data, 1000))1000 10Documentation that helps
Good documentation answers questions the code cannot. A short docstring states what a function does and what it expects. Comments explain why, not what: # cents, to avoid rounding errors is useful; # add 1 to i is noise. A README tells a new user how to install and run the program. Documentation is judged on being accurate, current and useful to its reader.
Automated tests
A test function with assert statements re-checks every case each time the code changes. An assert that is false stops the program with an AssertionError.
DISCOUNT_ITEMS = 10
def apply_discount(total, items):
if items >= DISCOUNT_ITEMS:
return total - total * 10 // 100
return total
def test_apply_discount():
assert apply_discount(4500, 9) == 4500 # just below the boundary
assert apply_discount(4500, 10) == 4050 # on the boundary
assert apply_discount(0, 0) == 0 # empty order
test_apply_discount()
print("All tests passed")All tests passedDebugging Tools
🎯 (SE-11-06, SE-11-07)
📌 Use debugging tools
- breakpoints
- single line stepping
- watches
- interfaces between functions
- debugging output statements
- debugging software available in an integrated development environment (IDE)
Breakpoints
A breakpoint is an instruction set within an IDE (such as Visual Studio Code or PyCharm) that pauses program execution at a specific line, allowing the engineer to inspect the current state of all variables and memory at that exact moment. For example, when debugging a loop that is supposed to accumulate a total but is producing an incorrect result, placing a breakpoint at the beginning of each iteration allows the engineer to examine the running total before and after each calculation — pinpointing precisely where the logic diverges from the expected behaviour without having to add print statements throughout the code.
Watches
A watch is a debugging feature that continuously monitors the value of a specific variable as the program executes, updating in real time with each statement. This is particularly useful when a variable is modified across multiple functions or modules. For example, setting a watch on a total_price variable allows an engineer to observe exactly at which line — and within which function — an incorrect GST calculation is being applied, rather than manually tracing through the entire codebase to find the source of the discrepancy.
Single Line Stepping
Single line stepping (also known as "step-through" debugging) executes the program one statement at a time, pausing after each line. This allows engineers to trace the exact path of execution through complex control structures — such as nested conditionals or recursive functions — verifying that each branch evaluates as intended. For example, stepping through an IF/ELIF/ELSE chain confirms which condition is evaluating to True for a particular input, revealing logical errors in the control flow that would be completely invisible during normal, uninterrupted execution.
def process_input(value):
try:
n = int(value)
if 1 <= n <= 100: return "Valid" # Boundary check
else: return "Out of Range"
except ValueError:
return "Abnormal Data" # Handling faulty data
# Example testing
print(process_input(100)) # Boundary: Valid
print(process_input("ABC")) # Abnormal: Handled
Debugging output statements
The simplest tool is a temporary statement that prints a value at a chosen point. It works anywhere, including where no debugger is available, but you must remove the statements afterwards and it can flood the screen inside a loop. Label each output so you can find it:
def apply_discount(total, items):
print(f"DEBUG apply_discount: total={total!r}, items={items!r}")
if items >= 10:
return total - total * 10 // 100
return totalInterfaces between functions
The interface of a function is what it expects in (parameters) and what it gives back (return value). Many faults are not inside a function but in the hand-over between two of them: the wrong units, the wrong order of arguments, or the wrong type. To debug an interface, print or watch the values as they cross the boundary, before and after the call.
def format_dollars(cents):
return f"${cents // 100}.{cents % 100:02d}"
total = 45.00 # dollars, but the next function expects cents
total = apply_discount(total, 10)
print("DEBUG total after discount:", total)
print(format_dollars(total))DEBUG total after discount: 41.0
Traceback (most recent call last):
...
ValueError: Unknown format code 'd' for object of type 'float'
(the exact traceback lines vary between Python versions)The debugging output shows 41.0, a float in dollars, arriving where whole cents are expected. The fault is in the caller, which passed dollars, not in either function.
Debugging software in an IDE
An integrated development environment (IDE) such as Visual Studio Code, PyCharm or Thonny includes a debugger that combines the tools above in one window: breakpoints, stepping controls, a variables panel, a watch list and a call stack. In Visual Studio Code the usual keys are F9 to toggle a breakpoint, F5 to run or continue, F10 to step over a line and F11 to step into a function. Python also ships a text debugger: writing breakpoint() in the code, or running python -m pdb file.py, starts it, where n runs the next line, s steps into a call, c continues and p expression prints a value.
Worked example: finding a bug with a breakpoint and a watch
This function should return the average of the marks. The expected result for [80, 70, 90] is 80.0.
def average(marks):
total = 0
for i in range(1, len(marks)):
total += marks[i]
return total / len(marks)
print(average([80, 70, 90]))53.333333333333336The result is wrong but there is no error message, so this is a logic error. Put a breakpoint on the line total += marks[i] and add a watch on i and total. Continue to the breakpoint each time and record the watch values:
| Hit | i | total (before the line runs) | marks[i] |
|---|---|---|---|
| 1 | 1 | 0 | 70 |
| 2 | 2 | 70 | 90 |
| (loop ends) | 160 |
The first value added is 70, not 80. The loop starts at i = 1, so marks[0] is never counted: 160 ÷ 3 = 53.33. The fix is range(len(marks)), or range(0, len(marks)). The same session in Python’s text debugger, using b 4 to set the breakpoint on that line and p i, total to inspect:
(Pdb) b 4
(Pdb) c
> avg.py(4)average()
-> total += marks[i]
(Pdb) p i, total
(1, 0)
(Pdb) c
> avg.py(4)average()
-> total += marks[i]
(Pdb) p i, total
(2, 70)total before it changes” earns more than “a breakpoint stops the program.” Match the tool to the fault: a watch for a variable that changes unexpectedly, single-line stepping to see which branch runs, debugging output where no debugger is available.Try it
Debugger: find the bug
Set a breakpoint, step through a program and read the watched variables. Enable JavaScript to use this tool. In short: a breakpoint pauses the program, stepping runs one line at a time and a watch shows how a variable changes.
Choosing Test Data
🎯 (SE-11-06, SE-11-07)
📌 Determine sets of suitable test data
- boundary values
- path coverage
- faulty and abnormal data
Good test data is chosen on purpose, not typed at random. A small, well-chosen set can find most errors. Imagine a program that accepts a mark from 1 to 100.
Test Data Selection
| Test Data Set | Analysis / Purpose | Example (Range 1-100) |
|---|---|---|
| Boundary | Tests the exact limits of valid input ranges. | 1 and 100 |
| Abnormal | Tests how the program handles invalid types or out-of-range data. | "Fifty", -5, 1000 |
| Path Coverage | Ensures every logical branch (every IF/ELSE statement) is executed. | Inputs that trigger both 'True' and 'False' cases. |
Boundary values
Boundary values are at the edges of what is valid. For every limit, test the value on it, the one just inside and the one just outside. For a mark from 0 to 100 that is −1, 0, 1 and 99, 100, 101. Most off-by-one errors, such as < written instead of <=, are only caught here. The boundaries between grade bands (49 and 50, 64 and 65, 84 and 85) need the same treatment.
Path coverage
Path coverage means the test data makes every route through the algorithm run at least once. Count the paths in the decision structure, then choose one input for each. This function has five paths (invalid, and four grades), so it needs at least five tests.
BEGIN Classify (mark)
IF mark < 0 OR mark > 100 THEN
RETURN "Invalid"
ELSE
IF mark >= 85 THEN
RETURN "High"
ELSE
IF mark >= 65 THEN
RETURN "Medium"
ELSE
IF mark >= 50 THEN
RETURN "Pass"
ELSE
RETURN "Fail"
ENDIF
ENDIF
ENDIF
ENDIF
END Classify (mark)Faulty and abnormal data
Abnormal data is the wrong type or is outside the range the program should accept, such as the text "fifty", -5 or 1000 for a mark. Faulty data is data that contains errors: a missing field, a corrupted value, or entries that contradict each other. A robust program rejects or reports both instead of crashing or accepting them.
Worked example: a test table for Classify
| # | Purpose | Test data | Expected result |
|---|---|---|---|
| 1 | Boundary: just below the range | -1 | Invalid |
| 2 | Boundary: lowest valid mark | 0 | Fail |
| 3 | Boundary: just above lowest | 1 | Fail |
| 4 | Boundary and path: below Pass | 49 | Fail |
| 5 | Boundary and path: lowest Pass | 50 | Pass |
| 6 | Boundary: top of Pass | 64 | Pass |
| 7 | Boundary and path: lowest Medium | 65 | Medium |
| 8 | Boundary: top of Medium | 84 | Medium |
| 9 | Boundary and path: lowest High | 85 | High |
| 10 | Boundary: highest valid mark | 100 | High |
| 11 | Boundary: just above the range | 101 | Invalid |
| 12 | Abnormal: text | "fifty" | Invalid |
| 13 | Abnormal: no value | None | Invalid |
| 14 | Abnormal: not a whole number | 72.5 | Invalid |
Every one of the five paths is covered, and every boundary is tested on both sides. Running the tests in Python, with the function written to check the type first:
def classify(mark):
if isinstance(mark, bool) or not isinstance(mark, int):
return "Invalid"
if mark < 0 or mark > 100:
return "Invalid"
elif mark >= 85:
return "High"
elif mark >= 65:
return "Medium"
elif mark >= 50:
return "Pass"
else:
return "Fail"
tests = [(-1, "Invalid"), (0, "Fail"), (1, "Fail"), (49, "Fail"), (50, "Pass"),
(64, "Pass"), (65, "Medium"), (84, "Medium"), (85, "High"), (100, "High"),
(101, "Invalid"), ("fifty", "Invalid"), (None, "Invalid"), (72.5, "Invalid")]
failed = 0
for mark, expected in tests:
actual = classify(mark)
if actual != expected:
failed += 1
print("FAIL", repr(mark), "expected", expected, "got", actual)
print(len(tests), "tests run,", failed, "failed")14 tests run, 0 failedIf the developer had written mark > 50 for the Pass band, test 5 would print FAIL 50 expected Pass got Fail, and the boundary test would have found the fault that a typical mark such as 72 never would.
The canteen program’s tests
| Test data | Type | Expected |
|---|---|---|
| 9 items totalling 4500 cents | Boundary (just below 10) | 4500 cents: no discount |
| 10 items totalling 4500 cents | Boundary (on 10) | 4050 cents: 10% off |
| 0 items (price 0 straight away) | Boundary (empty order) | Total $0.00 |
Price typed as abc or -5 | Abnormal | Message; asks again |
| A very large order (for example 100 000 items) | Volume | Correct total, no slowdown |
"abc". Add the expected result for each; a test without an expected result cannot pass or fail. Marks are lost for only typical values such as 50 and 75.Typical Errors and Their Causes
🎯 (SE-11-06, SE-11-07)
📌 Determine typical errors experienced when developing code, including syntax, logic and runtime, and explain their likely causes
Understanding common error types helps programmers identify and fix problems systematically:
Syntax Errors
Definition: Violations of the programming language's grammar rules. The interpreter/compiler cannot parse the code and refuses to run it.
Likely Causes: Missing colons, mismatched brackets, incorrect indentation (Python), typos in keywords.if x > 5 (missing colon at the end) or for i in range(10] (mismatched bracket types).
Logic Errors
Definition: The program runs without crashing, but produces wrong results because the algorithm is flawed.
Likely Causes: Wrong operator (using + instead of ×), off-by-one loop errors, incorrect conditional logic, unreachable code.A loop from 0 to 9 when you intended 1 to 10 (off-by-one error), or calculating tax as salary × 0.01 instead of salary × 0.10 (operator error).
Runtime Errors
Definition: The program starts running but crashes during execution due to impossible operations or resource exhaustion.
Likely Causes: Division by zero, accessing an array index that doesn't exist, null pointer dereference, stack overflow from infinite recursion, out of memory.score[index] where index is 10 but the array only has 5 elements, or result = 100 / denominator when denominator is 0.
What the computer reports
Each type of error appears at a different time, so a different tool finds it.
| Error type | When it appears | What you see | How to find it |
|---|---|---|---|
| Syntax | Before the program runs (Python reports it when the file is read) | An error message with a line number and no output at all | Read the message; check the line and the one before it |
| Runtime | While the program is running, at the moment the impossible operation happens | The program stops with an error message (a traceback) | Read the last line of the traceback; check the values on that line |
| Logic | Never reported by the computer | The program runs to the end but the output is wrong | Desk check, trace table, boundary test data, breakpoint and watch |
Real error messages and their causes
The last line of a Python error message names the type of error and describes it. The wording can differ slightly between versions.
| Code | Error message | Type | Likely cause |
|---|---|---|---|
if x > 5 | SyntaxError: expected ':' | Syntax | Missing colon at the end of the line |
for i in range(10]: | SyntaxError: closing parenthesis ']' does not match opening parenthesis '(' | Syntax | Mismatched brackets |
print("hi" | SyntaxError: '(' was never closed | Syntax | A bracket left open |
print(totl) | NameError: name 'totl' is not defined | Runtime | A misspelt variable name, or use before assignment |
100 / 0 | ZeroDivisionError: division by zero | Runtime | A divisor that can be 0, such as an empty list’s count |
marks[5] on 3 items | IndexError: list index out of range | Runtime | An index past the end, often an off-by-one |
int("abc") | ValueError: invalid literal for int() with base 10: 'abc' | Runtime | Converting text that is not a number |
"Total: " + 5 | TypeError: can only concatenate str (not "int") to str | Runtime | Mixing types without converting |
A logic error worked through
The program below runs without complaint, yet the answer is wrong. A student wants the average of two marks.
mark1 = 70
mark2 = 80
average = mark1 + mark2 / 2
print(average)110.0The expected answer is 75.0. Division is done before addition, so Python calculates 70 + (80 / 2). The cause is operator precedence; the fix is brackets: (mark1 + mark2) / 2. Other common causes of logic errors are off-by-one loops, the wrong comparison operator (< for <=), a variable not reset before a loop, and a condition that can never be true.
Spot the error
Classify each fault, then check
while count < 10with no colon.- A loop that adds prices but starts
totalat 1. names[len(names)]to get the last name.age = input("Age: ")followed byage + 1.
Answers. (1) Syntax error: missing colon. (2) Logic error: the run is fine but every total is one too high. (3) Runtime error (IndexError): the last index is len(names) - 1. (4) Runtime error (TypeError): input returns a string; convert with int().
Try it
Syntax, logic or runtime error?
Practise naming the type of error in a program. Enable JavaScript to use this tool. In short: a syntax error stops the program before it starts, a runtime error stops it part way and a logic error gives wrong results without stopping.