Programming Fundamentals

Mastering how to plan, design, and engineer software solutions through computational thinking, algorithm design, and structured data representation.

Year 11 — Preliminary ~10 weeks Outcomes: SE-11-01, 02, 03, 04, 06, 07
21Dot points
4Parts
6Outcomes
~10Weeks
Syllabus Part 1 Software development

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
1

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.

2

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.

3

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.

4

Development

The design is translated into a programming language like Python.Writing the 'if-else' logic to handle user login authentication.

5

Integration

Combining individual modules into a cohesive system.Connecting the 'User Interface' module to the 'Database Management' module.

6

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).

7

Installation

Deploying the software to the user.Providing a .dmg or .exe installer and a user manual to the client.

8

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.

StepWhat the team does for the canteen programWhat the step produces
1. Requirements definitionInterview 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 specificationsTurn 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. DesignSketch the mainline and subroutines in pseudocode (below) and choose the data to store: items, total.Pseudocode, a structure chart, a data dictionary
4. DevelopmentWrite each subroutine in Python from the design.Source code
5. IntegrationJoin 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 debuggingRun 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. InstallationCopy 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. MaintenanceNext 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.

Pseudocode: design
BEGIN Mainline
    GetOrder (items, total)
    ApplyDiscount (total, items)
    DisplayTotal (total)
END Mainline

The development and integration steps turn that design into Python. Each subroutine is written and tried on its own, then main joins them.

Python: development and integration
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:

Output
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.10

Check 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.

Requirements versus specifications Requirements say what the client needs in the client’s words (“staff must be able to reprint a receipt”). Specifications say how well and on what the software must do it, in technical terms (“responds in under a second on a 4 GB laptop”). Requirements come first and drive the specifications.

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.

Exam tip: “Explore” An explore question is open, so cover more than a list. Name the steps that matter to the scenario, say what each produces, and show how they connect: “the specification gives design its constraints; testing sends faults back to development.” Anchor the answer in one concrete project, as above. Common mistakes are treating requirements and specifications as the same step, and forgetting that maintenance continues after installation.

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

StrengthsLimitations
Everyone works from the same up-to-date codeBeginners find branching and merge conflicts confusing
Full history: you can see who changed what and roll back mistakesNeeds a reliable internet connection
Built-in review, issue tracking and automated testingPrivate code is stored on another company's servers
HSC writing sample "Online collaboration tools such as GitHub are now standard in the industry because they let distributed teams work on one codebase safely. Pull requests improve quality by requiring a peer review before code is merged, although teams must be trained to resolve merge conflicts."

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.

ToolBest known forWorth noting
GitHubHosting Git repositories, pull requests, issue tracking, automated workflows (GitHub Actions)Very large public community; free private repositories with limits
GitLabRepository hosting with built-in CI/CD; a pull request is called a merge requestCan be installed on an organisation’s own servers
BitbucketGit hosting from Atlassian that links to Jira issue trackingSuits 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 onceGood 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.

Terminal: one change from start to pull request
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 request

On 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.

What Git writes into the file
def apply_discount(total, items):
<<<<<<< HEAD
    if items >= 10:
=======
    if items >= 8:
>>>>>>> add-teacher-discount
        return total - total * 10 // 100

Everything 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

CriterionQuestions to ask
Fit to the taskDoes it provide version control, review and issue tracking, or only one of them?
Learning curveCan every team member use branching and merging after one lesson? Is there a graphical option?
Cost and limitsAre private repositories, storage and automation free for a school team?
Privacy and securityWhere is the code stored, and who can see it? Never commit passwords or API keys; remove one that slips in and change it.
IntegrationDoes it connect to the team’s editor, testing and project-tracking tools?
Exam tip: “Research and evaluate” A full-mark evaluate answer makes a judgement, not a list. Name a tool and a feature (pull requests), say what it achieves (peer review before merging), give a limitation (merge conflicts confuse beginners, or the code sits on another company’s servers) and end with a conclusion for the scenario given, such as “for a five-person school team, GitHub’s free private repositories and pull requests outweigh the learning curve.” Avoid answers that describe version control in general without saying why the team benefits.
Syllabus Part 2 Designing algorithms

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?

pseudocode
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.

pseudocode
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.

Pseudocode
CASEWHERE menuChoice evaluates to
    1: OUTPUT "New game"
    2: OUTPUT "Load game"
    3: OUTPUT "High scores"
    OTHERWISE: OUTPUT "Invalid choice"
END CASE

Nested 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.

Pseudocode
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
ENDIF
Count the ENDIFs Three IF 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.

pseudocode
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.

LoopTests the conditionBody runsUse it when
WHILE … ENDWHILEBefore each passZero or more timesYou do not know how many passes, and none may be needed (read until a sentinel)
REPEAT … UNTILAfter each passAt least onceThe body must run first, for example asking for a value until it is valid
FOR … NEXTCounts from start to finishA known number of timesYou know the count, or you are visiting every position of an array

REPEAT … UNTIL

Pseudocode
REPEAT
    OUTPUT "Enter a mark from 0 to 100: "
    INPUT mark
UNTIL mark >= 0 AND mark <= 100

FOR … NEXT with STEP

Pseudocode
FOR i = 1 TO 10 STEP 2
    OUTPUT i
NEXT i
// Output: 1 3 5 7 9

4. 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.

pseudocode
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.

DataKindWhy it is storedType
markInputThe value just entered; the loop tests itInteger
highestStateThe largest mark so far, compared with each new markInteger, starts at 0
passCountStateA running count of marks of 50 or moreInteger, starts at 0
Sentinel -1ConstantTells the loop that input has finished; never counted as a markInteger
Pseudocode
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 FindHighest

This 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 markmark > highest?highestmark >= 50?passCount
(start)00
72Yes72Yes1
45No72No1
91Yes91Yes2
50No91Yes3
-1loop ends913

The same design in Python reads the marks from a list, so it can be run and checked straight away.

python
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)
Output
Highest: 91
Passes: 3

Computational thinking

Computational thinking is the habit behind every design above. It has four parts, and you use all four whenever you plan an algorithm.

DecompositionBreaking a big problem into smaller parts you can solve separately, such as get input, process, report.
Pattern recognitionSpotting that a new problem resembles one you have solved, such as “find the largest” and “find the smallest”.
AbstractionKeeping the details that matter and ignoring the rest, such as treating a student as an ID and a mark.
Algorithm designWriting the exact steps, using sequence, selection and iteration, so anyone (or a computer) can follow them.
Exam tip: “Apply” An apply question gives you a situation and expects the ideas used on it, not a definition. If asked to design an algorithm for a scenario, name what is stored, use the correct NESA keywords with matching pairs (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.

▶ Watch & Learn

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

  1. Divide the problem into smaller sub-problems of the same kind.
  2. Conquer each sub-problem, by solving it directly when it is small enough (the base case) or by applying the same method again.
  3. 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:

Pseudocode
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:

Passlowhighmidlist[mid]ComparisonAction
10844444 < 57Discard the left half: low ← 5
25866363 > 57Discard the right half: high ← 5
35555757 = 57Found: 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.

python
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))
Output
5
-1

Worked 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:

PhaseLists 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]
python
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]))
Output
[3, 9, 10, 27, 38, 43, 82]

Why it is efficient

Items in the listLinear search, worst caseBinary search, worst case
99 comparisons4 comparisons
1 0001 000 comparisons10 comparisons
1 000 0001 000 000 comparisons20 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.

Python — Algorithmic Strategies
# 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.

python
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])
Output
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:

StepWhat the search doesResult
1Row 1: try column 1Safe, placed
2Row 2: try columns 1, 2, 3Columns 1 and 2 are attacked; column 3 is safe, placed
3Row 3: try columns 1 to 4Every column is attacked: dead end, so backtrack and remove the queen from row 2
4Row 2: try column 4Safe, placed
5Row 3: try columns 1, 2Column 1 is attacked; column 2 is safe, placed
6Row 4: try columns 1 to 4Every column is attacked: dead end, so backtrack; row 3 has no other safe column, so remove row 2’s queen as well
7Row 2 has no more columns, so remove row 1’s queen. Row 1: try column 2Safe, placed
8Rows 2, 3, 4Row 2 column 4, row 3 column 1, row 4 column 3 are all safe. Solution found
python
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)))
Output
[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 conquerBacktracking
Core ideaSplit into smaller versions of the same problem, solve, combineBuild a solution step by step and undo a step when it breaks a constraint
Sub-problemsIndependent; each half is solved on its ownDependent; each choice affects the choices after it
ExamplesBinary search, merge sortMaze paths, four queens, Sudoku
CostFast because the problem shrinks quicklyCan be slow because many dead ends may be tried
Exam tip: “Apply” When asked to apply divide and conquer or backtracking to given data, show the working, not just the answer. For binary search, list 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.
Always define a base case! Without a base case, the function calls itself indefinitely, causing a RecursionError: maximum recursion depth exceeded (stack overflow).
Python — Factorial: iterative vs recursive
# 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
Python — Fibonacci (recursive)
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
When to use recursion vs iteration Use recursion when the problem is naturally recursive (tree traversal, factorial, divide-and-conquer). Use iteration when you need performance — recursive calls create function stack frames that use more memory. Python's default recursion limit is 1000 calls.
▶ Watch & Learn

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:

Pseudocode: linear search
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 BEGIN has an END, every IF has an ENDIF, every WHILE an ENDWHILE, every FOR a NEXT.
  • 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.
StructureNESA 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)
Old-style pseudocode Many textbooks and websites use a combined else-if keyword, or write closers as two words (END and IF, END and WHILE, END and FOR). NESA does not; it uses the single keywords 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 symbolWhat it meansPseudocode it becomes
Terminator (rounded)Start or end of an algorithm or subroutineBEGIN and END
ParallelogramInput or outputINPUT age, OUTPUT total
RectangleA process such as a calculation or assignmenttotal ← total + price
DiamondA decision with labelled exitsIF … ENDIF, or the test of a loop
Rectangle with double side linesA call to a subprogramThe 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 Standard: Uncluttered Mainline Algorithms

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.

Refactoring Workshop — Creating an Uncluttered Mainline
// ❌ 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:

pseudocode
// ✅ 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

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

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 score

NESA pseudocode

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

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

Pseudocode
BEGIN DisplayResult (grade)
    OUTPUT "Your grade is ", grade
END DisplayResult (grade)

Python

python
def display_result(grade):
    print("Your grade is", grade)

def main():
    score = get_valid_score()
    grade = calculate_grade(score)
    display_result(grade)

main()
Sample run
Enter your score out of 100: 78
Your grade is Distinction

Passing 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

Pseudocode
BEGIN ShowGreeting (name)
    OUTPUT "Welcome, ", name, "!"
END ShowGreeting (name)

BEGIN Mainline
    ShowGreeting ("Alice")
    ShowGreeting ("Ben")
END Mainline

Python

python
def show_greeting(name):
    print("Welcome, " + name + "!")

show_greeting("Alice")
show_greeting("Ben")
Output
Welcome, Alice!
Welcome, Ben!

Several parameters, result passed back through a parameter

Pseudocode
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 Mainline

Python: an integer cannot be changed by the subroutine, so it returns the value

python
def multiply(a, b):
    return a * b

for i in range(1, 4):
    answer = multiply(i, 4)
    print(answer)
Output
4
8
12

A function that returns one value

Pseudocode
BEGIN CalculateGST (price)
    RETURN price * 0.10
END CalculateGST (price)

BEGIN Mainline
    total ← 100 + CalculateGST (100)
    OUTPUT total
END Mainline

Python

python
def calculate_gst(price):
    return price * 0.10

total = 100 + calculate_gst(100)
print(total)
Output
110.0
Exam tip: “Develop” A develop question asks you to produce a working design. Start with an uncluttered mainline that only names subroutines, then give each subroutine its own algorithm with the same name in BEGIN 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-downBottom-up
Starts withThe overall goalSmall, well-understood components
Tool that supports itStructure chart, then refinement of each boxA list of available modules and functions, joined by a mainline
StrengthKeeps the whole system in view; easy to divide work in a teamReuses tested code; early progress on the hard parts
RiskDetail is left until late, so a hard part may surprise youPieces may not fit together into the required whole
ExampleCanteen program: Get order, Apply discount, Display total, then refine eachReuse 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:

Empty circle on an arrowData moving between subroutines, usually passed as a parameter, such as pin or amount.
Filled circle on an arrowA flag or control variable, such as valid or cardRemoved, passed between subroutines.
Small diamondA decision: the subroutine is called only as the result of a binary or multi-way selection, or is optional.
Curved arrowRepetition: the subroutine, or set of subroutines, is executed several times.

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

Pseudocode
BEGIN Mainline
    GetOrder (items, total)
    ApplyDiscount (total, items)
    DisplayTotal (total)
END Mainline

Level 1: refine GetOrder

Pseudocode
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)
Level 2: refine ReadWholeNumber
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.

Abstraction in this example At level 0 you do not know, or need to know, how a whole number is checked. ReadWholeNumber hides that detail behind its name. Abstraction is what lets a team member use the subroutine without reading its code.
Exam tip: “Use” A use question about modelling tools usually gives a scenario and asks you to draw or complete a structure chart. Name each box as a verb and noun (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.

Scenario You're handed this algorithm with no explanation:
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 inputsWhat data the algorithm works onA list of numbers
Starting valuesWhat is being accumulated or comparedresult starts as the first item, so it tracks a “best so far”
The test inside the loopWhat counts as better or a matchlist[i] > result: bigger wins
What is returnedThe outputA 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.

Pseudocode
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:

imarks[i]marks[i] > 50?count
(start)0
145No0
260Yes1
350No1
endRETURN 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.

Pseudocode
BEGIN PayEmployee (salary, bonusPercent)
    tax ← CalculateTax (salary)
    pay ← salary - tax + ApplyBonus (salary, bonusPercent)
    RETURN pay
END PayEmployee (salary, bonusPercent)
CallPassed inParametersComes backUsed for
CalculateTax (salary)salarysalaryThe tax owed (one value)Subtracted from pay
ApplyBonus (salary, bonusPercent)salary, bonusPercentbasePay, percent (names may differ)The bonus amountAdded 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.

Exam tip: “Analyse” An analyse question gives you an algorithm you have not seen. Say its input, its output and its purpose in one sentence each, then support it with a desk check: draw a trace table for chosen data, show the values changing, and state what the result proves. Use data that includes a boundary. Do not just re-read the code line by line in words.

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.

ProcedureFunction
What it doesCarries out an actionCalculates and returns a value
How to spot itCalled as a statement on its own lineUsed inside an expression, e.g. total ← total + getPrice(item)
ExampleprintReceipt(order)calculateGST(price)

Functions vs Procedures

FeatureFunctionProcedure
Returns a value?Yes — always returns somethingNo — performs an action, returns None
Python keyworddef + return valuedef + no return (or bare return)
Use whenYou need a computed resultYou need a side effect (print, write file)
Examplecalculate_tax(income)print_report(data)

Return Values

Python — Single and multiple 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.

Pseudocode
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)
SubprogramProcedure or function?Evidence in the algorithm
ShowWelcomeProcedureStands alone on its own line; nothing is assigned or used from it
CalculateGSTFunctionIts result is assigned: gst ← CalculateGST (total)
DiscountFunctionIts result is used inside an expression: final - Discount (final)
PrintReceiptProcedureCalled 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:

python
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 store
Output
8.0
Total: 90
None

RETURN 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.

Flowcharts In a flowchart, a call to a subprogram is drawn as a rectangle with a double line on each side, labelled with the subprogram’s name and arguments. The subprogram has its own flowchart whose terminators carry the same name, so you can match call and detail.

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.

Prolog — Logic Paradigm Lab
% 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

python
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))
Output
44.0

Functional: expressions with no changing variables

python
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))
Output
44.0

Object-oriented: data and behaviour bundled in a class

python
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())
Output
44.0

Logic: 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.

Prolog
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.05

The 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.

ParadigmCore ideaYou describe…SuitsPython support
ImperativeCommands that change state step by stepHow to do itScripts, control code, most beginner programsLoops, assignment, functions
Object-orientedObjects bundle data with the methods that use itThings and what they can doLarge programs modelling real entitiesClasses, inheritance
LogicFacts and rules; the engine infers answersWhat is trueRule-based problems, knowledge basesNot built in; libraries exist
FunctionalPure functions, no changing stateWhat to computeData transformation, easy-to-test codeLambda, 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.

Exam tip: “Experiment” and paradigm questions Paradigm questions usually give you a short piece of code or a situation and ask you to identify the paradigm or compare two. Justify with a feature visible in the code: a class and methods (object-oriented), a loop changing a variable (imperative), facts and rules with no loop (logic), or a pure function with no side effects (functional). A common mistake is saying Python is “only” one paradigm; it supports several.

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.

Syllabus Part 3 Data for software engineering

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., #FFFFFF for white in CSS).
DecimalBinaryHexadecimalWhere you'll see it
101010ACounting, prices and marks
2551111 1111FFThe largest value in one byte; colour channels in #FF8800
1921100 0000C0The 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.
Why hexadecimal? One hex digit stands for exactly four bits, so a byte is always two hex digits. That makes long binary values, such as memory addresses and colour codes, much shorter and easier to read.

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.

BaseDigits usedPlace values (right to left)Example
Decimal (10)0 to 91, 10, 100, 1000214 = 2×100 + 1×10 + 4×1
Binary (2)0 and 11, 2, 4, 8, 16, 32, 64, 12811010110 = 128 + 64 + 16 + 4 + 2 = 214
Hexadecimal (16)0 to 9, then A to F1, 16, 256, 4096D6 = 13×16 + 6×1 = 214

Hexadecimal needs sixteen digits, so the letters A to F stand for the values 10 to 15:

Hex0123456789ABCDEF
Decimal0123456789101112131415
Binary0000000100100011010001010110011110001001101010111100110111101111

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.

DivisionQuotientRemainder
156 ÷ 2780
78 ÷ 2390
39 ÷ 2191
19 ÷ 291
9 ÷ 241
4 ÷ 220
2 ÷ 210
1 ÷ 201

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

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))            # 156
Output
0b10011100
0x9c
10011100
9C
156
156

Where 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 bitsDifferent valuesUnsigned range
4160 to 15
82560 to 255
1665 5360 to 65 535
324 294 967 2960 to 4 294 967 295
Quick self-check: four conversions
  1. Convert decimal 45 to binary.
  2. Convert hexadecimal B7 to decimal.
  3. Convert hexadecimal 2F to binary.
  4. 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.

Exam tip: “Investigate” and conversions Conversion questions are marked on working as well as the answer. Show the repeated division and the remainders in order, or the place values you added, and state the final answer with its base (for example 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:
  1. Start with the positive number: 5 = 00000101
  2. Invert all bits (One's Complement): 11111010 (each 0 becomes 1, each 1 becomes 0)
  3. Add 1 to the result: 11111010 + 1 = 11111011
  4. 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 ✓)

Python — Number Investigation
# 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
▶ Watch & Learn

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.

BitsSmallestLargest
4−87
8−128127
16−32 76832 767
32−2 147 483 6482 147 483 647

More worked examples (8 bits)

ValuePositive formInvertAdd 1Result
−500000101111110101111101111111011
−100000001111111101111111111111111
−2000010100111010111110110011101100
−12810000000 (no +128 in 8 bits)011111111000000010000000

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.

python
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)     # -10
Output
11111011
-5
11101100
-10

In 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
  1. Write −7 as a 4-bit two’s complement number.
  2. What decimal number is 10011010 in 8-bit two’s complement?
  3. 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.
Scenario A canteen app stores 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.

python
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 3
Output
65
a
H SC 3

Boolean

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.

python
mark = 50
passed = mark >= 50
print(passed, type(passed).__name__)    # True bool
Output
True bool

integer

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:

python
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.3
Output
0.30000000000000004
False
True
0.3

Never 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.

python
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.0
Output
0.10000000149011612
16777216.0

The 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.

python
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 dates
Output
14/03/2026
212

Storing 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.

DataBest typeReason
Student ID such as 10482Integer, or string if leading zeros matterWhole number; you never do arithmetic on it
Price in a canteenInteger number of centsAvoids floating point rounding
Temperature from a sensorReal (single precision is often enough)Has a fractional part; a few digits are enough
Is the user logged in?BooleanOnly two states
Enrolment dateDate and timeSorting and calculating differences
First initialChar (string of length 1)A single character
Exam tip: “Investigate” data types When asked to choose or justify a data type, name the type and give a reason tied to what is stored: “string, because a phone number has a leading zero and is never used in a calculation.” A one-word answer earns few marks. Watch for questions that ask for the storage size: single precision is 32 bits (4 bytes) and a date and time is 32 bits, as in NESA’s data dictionary example.

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.

VariableData typeFormat for displaySize in bytesSize for displayDescriptionExampleValidation
StudentIdIntegerNNNNN45Primary key; identifies a student1048210000 to 99999; unique
FirstNameStringXxxx…2020Student’s given namePriyaNot empty; letters only; first letter a capital
YearLevelIntegerNN12Current year of school11Must be 11 or 12
EnrolDateDate and TimeDD/MM/YYYY410Date the student enrolled05/02/2025Valid date, not in the future
FeesOwingFloating Point$NNN.NN47Fees still to be paid$45.50Decimal from 0 to 999.99
HasConsentBooleanX1 bit1Photo permissionYY or N
SubjectsArray (string)20 × subjectsN/ASubjects studiedSoftware EngineeringFrom 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:

ColumnQuestion it answersTip for writing it
VariableWhat is it called in the code?Use one consistent style, such as StudentId, and the same name in the program
Data typeWhat kind of value is it?Choose from the standard types: string, integer, floating point, Boolean, date and time
Format for displayHow does it look on screen?Use N for a digit and X for a letter, such as XXNNN
Size in bytesHow much storage does it need?Integer 4, single precision 4, date and time 4, Boolean 1 bit
Size for displayHow many characters are shown?Count decimal points, currency symbols and slashes as well
DescriptionWhat is it for?State the purpose in the user’s terms; note if it is a key
ExampleWhat does a real value look like?Use a realistic value that passes validation
ValidationWhat 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:

python
students = [
    {"StudentId": 10482, "FirstName": "Priya", "YearLevel": 11},
    {"StudentId": 10517, "FirstName": "Marcus", "YearLevel": 12},
]
for student in students:
    print(student["StudentId"], student["FirstName"])
Output
10482 Priya
10517 Marcus

Turning 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:

python
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, []))
Output
True False False
True False False

Worked 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.

VariableData typeFormat for displaySize in bytesSize for displayDescriptionExampleValidation
PriceCentsIntegerNNNN44Price of one item in cents450Whole number 1 to 9999
QuantityIntegerNN22How many of that item6Whole number 1 to 99
ItemCountIntegerNNN43Running count of items in the order10Whole number 0 or more
OrderTotalInteger$NNN.NN47Order total in cents, shown in dollars$35.10Whole number 0 or more
Exam tip: “Create” A create question on data dictionaries expects a table with the columns NESA lists, and realistic rows for the scenario. Give every variable a sensible data type, a size in bytes and a size for display (they are different), a real example, and a validation rule that can be tested. Marks are lost for missing columns, an example that breaks its own rule, and storing a phone number or postcode as an integer.

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}}
▶ Watch & Learn

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.)

python
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)
Output
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.

python
student = {"name": "Alice", "id": 12345, "gpa": 3.95}
print(student["name"], student["id"])
student["gpa"] = 4.0
print(student)
Output
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:

python
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)
Output
Documents
    Reports
    Photos
        2026
Music

Sequential 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.

python
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)
Output
Ava 72
Ben 85
Cai 64

The 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:

python
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))
Output
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

StructureAccessGood forWeakness
ArrayBy position (index)Lists and grids of the same typeAdding or removing in the middle is awkward
RecordBy field nameOne thing with mixed-type fieldsOne record only; combine records in an array
TreeFrom the root downHierarchies such as folders, and fast searchingMore complex to build and traverse
Sequential fileIn order, start to endStoring data after the program endsFinding one record means scanning
StackTop only (last in, first out)Undo, back button, function callsNo access to items below the top
Hash tableBy keyFast lookup by ID or nameUnordered; collisions must be handled
Exam tip: “Use” data structures Questions often give a scenario and ask which structure suits it, with a reason. Name the structure, then link it to how the data is accessed: “a stack, because undo must remove the most recent action first (last in, first out).” For code questions, check the index: an array of five items has positions 0 to 4, and index 5 causes a runtime error.
Syllabus Part 4 Developing solutions with code

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.

Python — Pass by value (immutable)
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.

Python — Pass by reference (mutable)
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
Python Mutability Rule Python passes a reference to the object. Immutable types (int, float, str, tuple) cannot be changed in place, so a function cannot change the caller's variable and the result behaves like pass-by-value. Mutable types (list, dict, set) can be changed in place, so changes inside a function are seen by the caller. Reassigning the parameter (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.
Python — Scope examples
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
Best Practice Prefer returning values over using 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 pseudocodePythonNote
total ← total + pricetotal = total + priceAssignment uses = in Python
IF … THEN … ELSE … ENDIFif … : … 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 CASEmatch … : case … : case _ :match needs Python 3.10 or later; if/elif also works
WHILE condition … ENDWHILEwhile condition:Pre-test loop
REPEAT … UNTIL conditionwhile True: … if condition: breakPython has no post-test loop, so the test is at the end
FOR i = 1 TO 5 … NEXT ifor i in range(1, 6):range stops before its second value
BEGIN name (a, b) … RETURN xdef name(a, b): … return xParameters are passed in the brackets
python
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")
Output
Load game

Worked 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.

Pseudocode
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)
python
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)))
Output
Average: 73.8
Highest: 90
Grade of highest: High Distinction

Standard 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.

python
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
Output
[80, 75, 90]
[]
Exam tip: “Apply” and code questions When you convert an algorithm, keep the same names and structure so a marker can match each line. Check that loops run the right number of times (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.

StructureWhat it storesPython example
Single-dimensional arrayA fixed row of values of one type, found by indexmarks = [72, 85, 64]
Multidimensional arrayA grid of values, found by row and columnseats[2][5] is row 2, seat 5
ListAn ordered collection that can grow and shrinkqueue.append("Priya")
TreeNodes in a hierarchy, each with one parentA folder structure, or a binary search tree of names
StackLast in, first out (LIFO)undo.append(action) then undo.pop()
Hash tableKey–value pairs with very fast lookupphone = {"Ava": "0412 345 678"}
Python — a stack for undo
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
Choosing a structure Use a hash table when you look items up by a key (a student ID), a stack when the most recent item matters most (undo), and a 2D array when the data is naturally a grid (a seating plan).

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.

python
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)
Output
['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.

python
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)
Output
['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.

python
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)
Output
Ava Ben Kim Leo Zoe

Stacks

The undo example above pushes and pops. Stacks also check that brackets match: push each opening bracket and pop when a closing one appears.

python
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)]"))
Output
True False

Hash 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.

python
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])
Output
55 72 None
[['Ava', 72], ['Dev', 55]]
Exam tip: “Implement” An implement question wants working code or precise pseudocode for a structure, not only its definition. Show how items are added and retrieved (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.
ExecutionWaterfallAgile
PlanningAll up frontA little before each sprint
Client involvementAt the start and the endThroughout, at every review
Response to changeSlow and costlyExpected and welcomed
Working softwareNear the endAfter the first sprint
DocumentationDetailed at every stageJust enough to support the work
HSC writing sample "Waterfall suits the school's exam-timetable system because its requirements are fixed by policy and must be signed off before development. Agile suits the student wellbeing app, where features depend on feedback, because each two-week sprint delivers a version students can test and comment on."

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

QuestionWaterfallAgile
How is the work organised?Requirements for the whole program written and signed off; then design; then code; then testSprint 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 finishedAt the end of sprint 1, working but basic
The manager asks for a teacher discountA change request: revise the requirements and design, then rework code and testsAdd it to the backlog and choose which sprint it goes in
Main riskThe finished program does not match what is now neededWithout 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.
Exam tip: “Compare” A compare answer covers similarities and differences, on the same points for both models. Use a structured set of criteria (planning, client involvement, response to change, when working software appears, documentation) and give a scenario-based judgement. A frequent mistake is describing each model in a separate paragraph without ever linking them with words such as “whereas”.

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

snake_caseVariables and function names use all lowercase with underscores: student_name, calculate_average(), max_score.
UPPER_CASEConstants (values that never change) use all caps: MAX_SCORE = 100, PI = 3.14159, DATABASE_URL.
PascalCaseClass names use capitalised words with no underscores: BankAccount, StudentRecord, DataProcessor.
_privateA single leading underscore is a convention (not enforced) indicating the variable/method is intended for internal use: _validate().

Descriptive Naming — Good vs Bad

Bad NameGood NameWhy It's Better
xstudent_ageDescribes what the variable stores
nnum_studentsClear abbreviation with context
dataexam_scoresSpecific about what data it holds
flagis_logged_inBoolean names start with is/has/can
temptemperature_celsiusIncludes units for clarity
calc()calculate_gst(price)Name tells you what it calculates
Variable Declaration Best Practices
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

python
def f(a, b):
    return a * b * 0.1

Version B

python
GST_RATE = 0.10

def gst_amount(price, quantity):
    """Return the GST in dollars for one line item."""
    return price * quantity * GST_RATE
CriterionVersion AVersion B
FunctionalityCorrect output for GST, but no way to tell that from the codeSame output, and the purpose is stated
PerformanceOne multiplication chain: fastSame cost: fast
Readabilityf, a, b and a bare 0.1 hide the meaningDescriptive names and a named constant explain themselves
Quality of documentationNoneA 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:

MethodWhat it isExample
Functional testingChecking each function of the software against the specificationEntering 10 items and confirming the 10% discount is applied
Acceptance testingThe client or users confirm the software meets their needs before accepting itThe canteen manager works through a real lunch order
Live dataTesting with real data from actual operationUsing last week’s real orders
Simulated dataTesting with made-up data that imitates real dataGenerating 500 pretend orders, including extreme values
Beta testingRelease to a limited group of real users outside the development teamTwo canteens use version 0.9 for a week and report problems
Volume testingTesting with a large amount of data or many usersFeeding 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:

python
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))
Output
1000 10

Documentation 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.

python
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")
Output
All tests passed
Exam tip: “Test and evaluate” This verb has two parts and needs both. Test: name the test data, the expected result and the actual result. Evaluate: use all four criteria (functionality, performance, readability, quality of documentation) with a piece of evidence for each and finish with a judgement, such as “the solution is correct but its unclear names make it costly to maintain.” Answers that only say “it works” do not evaluate.

Debugging 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.

Python — Evaluative Testing and Debugging
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:

python
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 total

Interfaces 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.

python
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))
Output
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.

python
def average(marks):
    total = 0
    for i in range(1, len(marks)):
        total += marks[i]
    return total / len(marks)

print(average([80, 70, 90]))
Output
53.333333333333336

The 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:

Hititotal (before the line runs)marks[i]
11070
227090
(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:

Python debugger session
(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)
Exam tip: “Use” debugging tools Say what the tool does and what you would look for with it. “A breakpoint pauses the program at line 4 so I can read the value of 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.
Tip Test just inside, on and just outside each boundary: 0, 1, 2 and 99, 100, 101. Off-by-one errors are among the most common bugs, and boundary values are what catch them.

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.

Pseudocode
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

#PurposeTest dataExpected result
1Boundary: just below the range-1Invalid
2Boundary: lowest valid mark0Fail
3Boundary: just above lowest1Fail
4Boundary and path: below Pass49Fail
5Boundary and path: lowest Pass50Pass
6Boundary: top of Pass64Pass
7Boundary and path: lowest Medium65Medium
8Boundary: top of Medium84Medium
9Boundary and path: lowest High85High
10Boundary: highest valid mark100High
11Boundary: just above the range101Invalid
12Abnormal: text"fifty"Invalid
13Abnormal: no valueNoneInvalid
14Abnormal: not a whole number72.5Invalid

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:

python
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")
Output
14 tests run, 0 failed

If 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 dataTypeExpected
9 items totalling 4500 centsBoundary (just below 10)4500 cents: no discount
10 items totalling 4500 centsBoundary (on 10)4050 cents: 10% off
0 items (price 0 straight away)Boundary (empty order)Total $0.00
Price typed as abc or -5AbnormalMessage; asks again
A very large order (for example 100 000 items)VolumeCorrect total, no slowdown
Exam tip: “Determine” test data A determine question gives a specification and expects you to choose data, not just name categories. Give exact values and label each with its purpose (boundary, path, abnormal). For a range 1 to 100, write 0, 1, 100 and 101, and add an abnormal value such as "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 typeWhen it appearsWhat you seeHow to find it
SyntaxBefore the program runs (Python reports it when the file is read)An error message with a line number and no output at allRead the message; check the line and the one before it
RuntimeWhile the program is running, at the moment the impossible operation happensThe program stops with an error message (a traceback)Read the last line of the traceback; check the values on that line
LogicNever reported by the computerThe program runs to the end but the output is wrongDesk 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.

CodeError messageTypeLikely cause
if x > 5SyntaxError: expected ':'SyntaxMissing colon at the end of the line
for i in range(10]:SyntaxError: closing parenthesis ']' does not match opening parenthesis '('SyntaxMismatched brackets
print("hi"SyntaxError: '(' was never closedSyntaxA bracket left open
print(totl)NameError: name 'totl' is not definedRuntimeA misspelt variable name, or use before assignment
100 / 0ZeroDivisionError: division by zeroRuntimeA divisor that can be 0, such as an empty list’s count
marks[5] on 3 itemsIndexError: list index out of rangeRuntimeAn index past the end, often an off-by-one
int("abc")ValueError: invalid literal for int() with base 10: 'abc'RuntimeConverting text that is not a number
"Total: " + 5TypeError: can only concatenate str (not "int") to strRuntimeMixing 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.

python
mark1 = 70
mark2 = 80
average = mark1 + mark2 / 2
print(average)
Output
110.0

The 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
  1. while count < 10 with no colon.
  2. A loop that adds prices but starts total at 1.
  3. names[len(names)] to get the last name.
  4. age = input("Age: ") followed by age + 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.

Exam tip: “Determine” typical errors To determine an error, name its type, point to the line, and explain the cause. “Logic error: the condition on line 6 uses > instead of >=, so a mark of exactly 50 is treated as a fail” scores; “there is a bug on line 6” does not. Match the type to when it shows up: a syntax error stops the program before it starts, a runtime error stops it partway, and a logic error never stops it.