B2.1.1 Construct, trace programs with variables (AO3)

B2.1.1_1 Data types: Boolean, char, decimal, integer, string

Example Program (Python)

# Declare variables with appropriate data types
student_name = "Alice" # String: Stores textual data
is_passing = True # Boolean: True/False for logical conditions
grade1 = 85 # Integer: Whole number for grades
grade2 = 90
weight = 75.5 # Decimal (float): For precise numerical values
initial = 'A' # Char (string of length 1): Single character
# Calculate average grade
average = (grade1 + grade2) / 2
# Output results
print(f"Student: {student_name}, Initial: {initial}")
print(f"Average Grade: {average}")
print(f"Passing: {is_passing}")

Tracing the Program

  • Initialize: student_name = "Alice", is_passing = True, grade1 = 85, grade2 = 90, weight = 75.5, initial = 'A'
  • Calculate: average = (85 + 90) / 2 = 87.5
  • Output: Prints "Student: Alice, Initial: A", "Average Grade: 87.5", "Passing: True"

Boolean

  • Stores True or False for logical conditions
  • Example: is_passing = grade >= 60

Char

  • Single character (in Python, a string of length 1)
  • Example: initial = 'A'

Decimal (Float)

  • Represents numbers with decimal points
  • Example: weight = 75.5 for precise measurements

Integer

  • Whole numbers for counts or discrete values
  • Example: grade1 = 85

String

  • Text data for names, descriptions, etc.
  • Example: student_name = "Alice"

Purpose

  • Selecting appropriate data types ensures accurate data storage, manipulation, and program functionality
  • Tracing verifies program logic by tracking variable changes and confirming expected outputs

B2.1.2 Construct programs for substring manipulation (AO3)

B2.1.2_1 Identify, extract, alter, concatenate, replace substrings

Identify

  • Locate specific substrings using methods like find() or index()
  • Example: text = "Hello World"; pos = text.find("World") returns 6

Extract

  • Retrieve a portion using slicing or substring methods
  • Example: text = "Hello World"; substring = text[6:11] extracts "World"

Alter

  • Modify parts by creating a new string with changes
  • Example: text = "hello"; text = text.upper() results in "HELLO"

Concatenate

  • Combine strings using + operator or join() method
  • Example: first = "Hello"; last = "World"; full = first + " " + last

Replace

  • Substitute a substring using replace() method
  • Example: text = "Hello World"; new_text = text.replace("World", "Python")

Example Program (Python)

# Program to manipulate a string
text = "Welcome to IB Computer Science"
# Identify: Find position of substring
pos = text.find("Computer")
print(f"Position of 'Computer': {pos}") # Output: 11
# Extract: Get substring
substring = text[11:19]
print(f"Extracted substring: {substring}") # Output: Computer
# Alter: Convert to uppercase
upper_text = substring.upper()
print(f"Uppercase substring: {upper_text}") # Output: COMPUTER
# Concatenate: Add prefix
new_text = "Subject: " + text
print(f"Concatenated: {new_text}") # Output: Subject: Welcome to IB Computer Science
# Replace: Change substring
replaced_text = text.replace("Computer Science", "Programming")
print(f"Replaced text: {replaced_text}") # Output: Welcome to IB Programming

Construction and Tracing

  • Construct: Write code to perform substring operations based on requirements
  • Trace: Step through the code, tracking variable values:
    • text = "Welcome to IB Computer Science"
    • pos = 14 (from find("Computer"))
    • substring = "Computer" (from slicing [14:22])
    • upper_text = "COMPUTER" (from upper())
    • new_text = "Subject: Welcome to IB Computer Science" (from concatenation)
    • replaced_text = "Welcome to IB Programming" (from replace())

Use Case

  • A program processing email addresses to extract usernames (e.g., user@domain.com → user)
  • Concatenate with a greeting, and replace domains for standardization
  • Ensuring correct manipulation through tracing

B2.1.3 Describe exception handling (AO2)

B2.1.3_1 Failure points: unexpected inputs, resource unavailability, logic errors

Unexpected Inputs

  • Occurs when user data does not match expected format or type
  • Example: Entering "abc" when expecting an integer
  • Can cause crashes (e.g., ValueError) or incorrect behavior

Resource Unavailability

  • Failure due to inaccessible resources (files, databases, network)
  • Example: Attempting to open a non-existent file
  • Results in FileNotFoundError, disrupting execution

Logic Errors

  • Errors in program logic (division by zero, out-of-bounds access)
  • Example: Dividing by a variable that equals zero
  • Causes runtime errors or incorrect outputs

B2.1.3_2 Role in program development

Purpose

  • Exception handling ensures programs gracefully handle errors
  • Improves robustness and user experience
  • Prevents crashes by catching and managing errors
  • Allows programs to recover or fail gracefully

Benefits in Development

  • Enhances reliability by anticipating and addressing potential failure points
  • Improves debugging by providing clear error messages and controlled error handling
  • Supports user-friendly applications by informing users of issues without abrupt termination

Use in Testing

  • Helps developers identify and handle edge cases during testing
  • Ensures comprehensive error management
  • Example: Testing a calculator program for division by zero

B2.1.3_3 Constructs: try/catch (Java), try/except (Python), finally block

Try/Catch (Java)

try {
    int result = 10 / 0; // Potential ZeroDivisionError
} catch (ArithmeticException e) {
    System.out.println("Error: Division by zero");
}
  • Catches specific exceptions to prevent crashes

Try/Except (Python)

try:
    result = 10 / 0 # Potential ZeroDivisionError
except ZeroDivisionError:
    print("Error: Division by zero")
  • Manages runtime errors, allowing program continuation

Finally Block (Java and Python)

try:
    file = open("data.txt", "r")
    content = file.read()
except FileNotFoundError:
    print("Error: File not found")
finally:
    file.close() # Ensures file is closed
  • Executes code regardless of whether an exception occurs
  • Often used for cleanup tasks (e.g., closing files)
  • Ensures resources are properly released, preventing leaks

Use Case

  • A Python program reading user input for a calculation uses try/except to handle invalid inputs
  • Finally block logs the operation's completion
  • Ensures robustness and proper resource management

B2.1.4 Construct, use debugging techniques (AO3)

B2.1.4_1 Techniques: trace tables, breakpoints, print statements, step-by-step execution

Trace Tables

  • Table tracking variable values at each step
  • Identifies logic errors or unexpected behavior
  • Example: For a loop calculating sum:
total = 0
for i in range(1, 4):
    total += i
print(total)
Step i total
1 1 1
2 2 3
3 3 6

Breakpoints

  • Pause execution at specific lines to inspect state
  • Used with debuggers (e.g., Python's pdb, IDEs)
  • Example: Setting breakpoint at total += i to check values
  • Pinpoints where errors occur

Print Statements

  • Insert print() to output variable values
  • Quick monitoring without debugger
  • Example:
total = 0
for i in range(1, 4):
    total += i
    print(f"i={i}, total={total}")
  • Output: i=1, total=1, i=2, total=3, i=3, total=6

Step-by-Step Execution

  • Execute code line-by-line using debugger
  • Observe behavior and variable changes in real-time
  • Tools: pdb, VS Code, PyCharm debuggers
  • Identifies exact lines where errors occur

Construction and Use

# Example: Debug a program to find the maximum number
numbers = [4, 7, 2, 9, 1]
max_num = numbers[0]
for num in numbers:
    print(f"Checking num={num}, max_num={max_num}") # Print statement
    if num > max_num:
        max_num = num
print(f"Maximum: {max_num}")
Step num max_num
1 4 4
2 7 7
3 2 7
4 9 9
5 1 9

B2.2.1 Compare static vs dynamic data structures (AO3)

B2.2.1_1 Memory allocation, resizing mechanisms

Static Data Structures

Memory Allocation
  • Memory allocated at compile time
  • Fixed size that cannot change during runtime
  • Example: numbers = [0] * 5 creates a Python list with 5 elements
Resizing Mechanism
  • Cannot be resized dynamically
  • Size is set during declaration
  • To change size, a new structure must be created and data copied

Dynamic Data Structures

Memory Allocation
  • Memory allocated at runtime
  • Flexible size adjustments as needed
  • Example: numbers = [1, 2, 3]; numbers.append(4) dynamically adds an element
Resizing Mechanism
  • Automatically resizes when capacity exceeded
  • Allocates new memory and copies data
  • Example: Python lists double capacity when full

B2.2.1_2 Pros, cons: speed, memory, flexibility

Aspect Static Data Structures Dynamic Data Structures
Speed Faster access (O(1)) due to contiguous memory and fixed size Slower due to resizing overhead (O(n) in worst case)
Memory More efficient, no overhead for resizing Higher overhead for resizing and extra capacity
Flexibility Inflexible, fixed size Highly flexible, grows/shrinks as needed
Use Case Fixed number of student grades (exactly 10) Tracking user inputs in a chat application

Static Pros

  • Faster access due to contiguous memory
  • Memory-efficient with no overhead

Static Cons

  • Inflexible size cannot change
  • Wasted space if not fully utilized

Dynamic Pros

  • Flexible size adjusts as needed
  • Easy to use with automatic memory management

Dynamic Cons

  • Slower due to resizing overhead
  • Higher memory usage for extra capacity

B2.2.2 Construct programs with arrays, lists (AO3)

B2.2.2_1 1D/2D arrays, ArrayLists (Java)

1D Arrays

  • Fixed-size, contiguous collection of elements
  • Accessed by index
# 1D array using Python list
grades = [85, 90, 78, 92]
grades[2] = 80 # Update element
print(grades[0]) # Access: 85
  • Use: Store sequences like student grades

2D Arrays

  • Matrix-like structure with rows and columns
  • Accessed by two indices
# 2D array using nested lists
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
matrix[1][2] = 10 # Update element
print(matrix[0][1]) # Access: 2
  • Use: Represent grids, tables, or seating charts

ArrayLists (Java)

  • Dynamic, resizable list in Java
  • Part of java.util package
import java.util.ArrayList;
// Initialize ArrayList
ArrayList<Integer> scores = new ArrayList<>();
scores.add(85); // Add element
scores.add(90);
scores.set(1, 95); // Update element
System.out.println(scores.get(0)); // Access: 85
  • Use: Store dynamic collections like user inputs

B2.2.2_2 1D/2D lists (Python)

1D Lists

  • Python's built-in dynamic lists
  • Resizable and can store mixed data types
# 1D list
names = ["Alice", "Bob", "Charlie"]
names.append("David") # Add element
names[1] = "Beth" # Update element
print(names[0]) # Access: Alice
  • Use: Store dynamic sequences like customer names

2D Lists

  • Nested lists representing a grid or table
  • Dynamic resizing capabilities
# 2D list
grid = [[0, 1, 0], [1, 0, 1], [0, 1, 0]]
grid[1][1] = 2 # Update element
print(grid[0][2]) # Access: 0
grid.append([1, 1, 1]) # Add new row
  • Use: Represent dynamic matrices like game boards

B2.2.2_3 Add, remove, traverse dynamic list elements

Add Elements

  • Append or insert elements to expand size
students = ["Alice", "Bob"]
students.append("Charlie") # Add to end
students.insert(1, "Beth") # Add at index 1
print(students) # ['Alice', 'Beth', 'Bob', 'Charlie']

Remove Elements

  • Remove by value, index, or clear entire list
students = ["Alice", "Beth", "Bob", "Charlie"]
students.remove("Beth") # Remove by value
students.pop(1) # Remove by index (Bob)
print(students) # ['Alice', 'Charlie']

Traverse Elements

  • Iterate through elements to process or analyze
scores = [85, 90, 78, 92]
for score in scores:
    print(f"Score: {score}")
# Access by index
for i in range(len(scores)):
    scores[i] += 5 # Add 5 to each
print(scores) # [90, 95, 83, 97]

Use Case

  • A program managing a student roster:
    • Adds new students (append)
    • Removes dropouts (remove)
    • Traverses the list to calculate average grades or print names
  • Ensures dynamic updates and efficient data processing

B2.2.3 Explain stack (LIFO) (AO2)

B2.2.3_1 Operations: push, pop, peek, isEmpty

Push

  • Adds element to the top of the stack
  • Example: Adding a plate to a stack of plates
stack = []
stack.append(5) # Push 5 onto stack

Pop

  • Removes and returns the top element
  • Example: Taking the top plate off
top = stack.pop() # Removes and returns 5

Peek

  • Returns top element without removing it
  • Example: Checking the top plate
top = stack[-1] if stack else None # Views top element

isEmpty

  • Checks if stack is empty
  • Returns True if no elements remain
is_empty = len(stack) == 0 # Returns True if empty

B2.2.3_2 Impact on performance, memory

Performance

  • Push/Pop: O(1) time complexity
  • Operations occur at the top (constant-time access)
  • Peek/isEmpty: O(1) time complexity
  • Only access top element or check length
  • Example: Fast for function call management

Memory

  • Uses contiguous or dynamic memory
  • Minimal overhead for simple operations
  • Dynamic resizing may cause occasional O(n) operations
  • Example: Function call stack uses small, fixed space per element

Trade-offs

  • Efficient for LIFO operations but limited for random access or searching (O(n))
  • Memory-efficient for sequential data but may waste space if underused

B2.2.3_3 Appropriate stack use

Function Call Stack

  • Manages function calls and returns
  • Tracks execution context
  • Example: Python's call stack in recursion

Undo/Redo Operations

  • Tracks actions in reverse order
  • Example: Text editor undo feature

Expression Evaluation

  • Processes expressions or validates syntax
  • Example: Checking balanced parentheses

Backtracking Algorithms

  • Supports algorithms like depth-first search
  • Example: Solving a maze by storing visited paths

Python Example

# Stack for checking balanced parentheses
def is_balanced(expression):
    stack = []
    for char in expression:
        if char == '(':
            stack.append(char) # Push
        elif char == ')':
            if len(stack) == 0: # Check isEmpty
                return False
            stack.pop() # Pop
    return len(stack) == 0
print(is_balanced("(a + b)")) # Output: True
print(is_balanced("(a + b))")) # Output: False

Appropriateness

  • Ideal for LIFO access, reversing actions, or managing nested operations
  • Inappropriate for random access or frequent searching

B2.2.4 Explain queue (FIFO) (AO2)

B2.2.4_1 Operations: enqueue, dequeue, front, isEmpty

Enqueue

  • Adds element to the back of the queue
  • Example: Adding a customer to a ticket line
queue = []
queue.append(10) # Enqueue 10

Dequeue

  • Removes and returns the front element
  • Example: Serving the first customer
front = queue.pop(0) # Dequeue first element (10)

Front

  • Returns the front element without removing
  • Example: Checking the next customer
front = queue[0] if queue else None # View front element

isEmpty

  • Checks if queue is empty
  • Returns True if no elements remain
is_empty = len(queue) == 0 # Returns True if empty

B2.2.4_2 Impact on performance, memory

Performance

  • Enqueue: O(1) using dynamic list (append)
  • Dequeue: O(n) in basic Python list due to shifting
  • Optimized implementations (collections.deque) achieve O(1)
  • Front/isEmpty: O(1) time complexity
# Optimized implementation
from collections import deque
queue = deque()
queue.append(10) # O(1)
queue.popleft() # O(1)

Memory

  • Uses contiguous or dynamic memory
  • Overhead for resizing in dynamic implementations
  • Efficient for sequential processing
  • May waste memory if pre-allocated capacity is underused
  • Example: Print job queue uses minimal space per job

Trade-offs

  • Fast for FIFO operations but inefficient for random access (O(n))
  • Optimized implementations like deque improve performance

B2.2.4_3 Appropriate queue use

Task Scheduling

  • Manages tasks in arrival order
  • Example: CPU process scheduling

Event Handling

  • Processes events/messages in order received
  • Example: Server queueing user requests

Breadth-First Search

  • Explores nodes level by level
  • Example: Finding shortest path in a maze

Buffering

  • Handles data streams
  • Example: Video player queueing frames

Python Example

from collections import deque
# Queue for processing print jobs
def process_print_jobs(jobs):
    queue = deque()
    for job in jobs:
        queue.append(job) # Enqueue job
    while len(queue) > 0: # Check isEmpty
        current_job = queue.popleft() # Dequeue front job
        print(f"Processing: {current_job}")
    return len(queue) == 0
print_jobs = ["Doc1.pdf", "Doc2.pdf", "Doc3.pdf"]
print(process_print_jobs(print_jobs)) # Processes each job, returns True

Appropriateness

  • Ideal for FIFO scenarios, processing tasks in order of arrival
  • Inappropriate for LIFO or random access, where stacks or arrays are better

B2.3.1 Construct programs implementing correct instruction sequence (AO3)

B2.3.1_1 The impact of instruction order on program functionality

Why Order Matters

  • Programs execute instructions sequentially — each line runs in the order it appears
  • Changing the order of instructions can produce entirely different (often incorrect) results
  • Variables must be declared and assigned before they are used
  • Computations that depend on previous results must appear after those results are computed

Incorrect Order

# ERROR: using average before it is calculated
print(f"Average: {average}") # NameError!
score1 = 80
score2 = 90
average = (score1 + score2) / 2
  • Causes a NameError: average is not yet defined when printed

Correct Order

# Correct: compute before printing
score1 = 80
score2 = 90
average = (score1 + score2) / 2
print(f"Average: {average}") # Output: 85.0
  • Variables are defined before use — program runs correctly

B2.3.1_2 Ways to avoid errors: infinite loops, deadlock, incorrect output

Infinite Loops

  • A loop that never terminates because the exit condition is never met
  • Cause: missing update to the loop variable, or logic error in condition
  • Prevention: ensure the loop variable is modified inside the loop body and the condition will eventually be false
# INFINITE — count never changes
count = 0
while count < 5:
    print("Hello") # count is never incremented!

# FIXED
count = 0
while count < 5:
    print("Hello")
    count += 1 # Loop variable updated

Deadlock

  • Two or more processes are each waiting for the other to release a resource — neither can proceed
  • Common in concurrent / multi-threaded programs
  • Prevention: use consistent resource-locking order, timeouts, or avoid circular dependencies between processes
# Conceptual example (pseudo-code)
# Process A holds Lock1, waits for Lock2
# Process B holds Lock2, waits for Lock1
# → deadlock: neither can continue

# Prevention: always acquire locks in the same order
# Both processes acquire Lock1 first, then Lock2

Incorrect Output

  • Program runs without errors but produces wrong results
  • Caused by logic errors: wrong formula, off-by-one index, incorrect operator
  • Prevention: trace tables, unit testing, careful code review
# Off-by-one error example
scores = [70, 80, 90]
# WRONG: misses last element
for i in range(len(scores) - 1):
    print(scores[i])

# CORRECT
for i in range(len(scores)):
    print(scores[i])

General Best Practices

  • Plan program logic with pseudocode or flowcharts before coding
  • Use trace tables to step through logic manually
  • Test with boundary values (e.g., 0, negative numbers, empty lists)
  • Use meaningful variable names to reduce logic mistakes

B2.3.2 Construct programs with selection structures (AO3)

B2.3.2_1 if, else, elif / else if — conditional execution

What is Selection?

  • Selection allows a program to choose which block of code to execute based on a condition
  • Enables programs to respond differently to different inputs or states
  • Core constructs: if, else, elif (Python) / else if (Java)

Python: if / elif / else

grade = 74

if grade >= 90:
    print("A")
elif grade >= 80:
    print("B")
elif grade >= 70:
    print("C") # Output: C
else:
    print("Fail")

Java: if / else if / else

int grade = 74;

if (grade >= 90) {
    System.out.println("A");
} else if (grade >= 80) {
    System.out.println("B");
} else if (grade >= 70) {
    System.out.println("C"); // Output: C
} else {
    System.out.println("Fail");
}

B2.3.2_2 Boolean operators (AND, OR, NOT) and relational operators (<, <=, >, >=, ==, !=)

Relational Operators

  • == equal to    != not equal to
  • < less than    <= less than or equal
  • > greater than    >= greater than or equal
  • Always produce a Boolean result (True or False)

Boolean Operators

  • AND — both conditions must be True
  • OR — at least one condition must be True
  • NOT — inverts a Boolean value
  • Used to combine multiple conditions in a single if statement

Combined Example (Python)

age = 17
has_permission = True
score = 55

# AND: both conditions required
if age >= 16 and has_permission:
    print("Access granted") # Output: Access granted

# OR: either condition sufficient
if score < 40 or score > 100:
    print("Invalid score")
else:
    print("Score accepted") # Output: Score accepted

# NOT: invert condition
if not has_permission:
    print("Access denied")
else:
    print("Welcome!") # Output: Welcome!

Use Case

  • A login system checks: if username == stored_name and password == stored_pass
  • A ticket system applies discounts: if age < 12 or age >= 65
  • A sensor triggers an alert: if temperature > 100 and pressure > 200

B2.3.3 Construct programs with looping structures (AO3)

B2.3.3_1 Counted loops vs conditional loops

Counted Loops (for)

  • Repeat a known, fixed number of times
  • Best when the number of iterations is determined before the loop starts
  • Uses a loop variable that steps through a range or collection
# Print squares of 1 to 5
for i in range(1, 6):
    print(f"{i}² = {i**2}")
# Output: 1²=1, 2²=4, 3²=9, 4²=16, 5²=25

# Traverse a list
names = ["Alice", "Bob", "Carol"]
for name in names:
    print(f"Hello, {name}")

Conditional Loops (while)

  • Repeat while a condition remains True
  • Best when the number of iterations is not known in advance
  • Loop variable must be updated inside the loop to avoid infinite loops
# Keep asking until valid input
user_input = ""
while user_input != "quit":
    user_input = input("Enter command: ")
    print(f"You entered: {user_input}")
print("Session ended")

B2.3.3_2 Conditional statements within loops using Boolean and relational operators

Example: Filtering and Accumulating

# Count and sum only passing grades (>= 50)
grades = [72, 45, 88, 33, 91, 60]
total = 0
count = 0
for grade in grades:
    if grade >= 50: # Condition inside loop
        total += grade
        count += 1
if count > 0:
    print(f"Passing average: {total / count:.1f}") # Output: 77.8

Example: Early Exit with Boolean Operators

# Search for first student with grade A (>= 90) who is also enrolled
students = [("Alice", 85, True), ("Bob", 92, True), ("Carol", 95, False)]
found = False
i = 0
while i < len(students) and not found:
    name, grade, enrolled = students[i]
    if grade >= 90 and enrolled:
        print(f"Top student: {name} ({grade})") # Output: Bob (92)
        found = True
    i += 1

Trace Table for Filtering Example

gradegrade >= 50totalcount
72True721
45False721
88True1602
33False1602
91True2513
60True3114
  • Final average: 311 / 4 = 77.75

B2.3.4 Construct functions and modularization (AO3)

B2.3.4_1 Functions — reusable blocks with different inputs

What is a Function?

  • A named block of code that performs a specific task and can be called multiple times
  • Accepts parameters (inputs) and optionally returns a value
  • Eliminates repeated code — define once, call many times with different arguments

Python Functions

def calculate_average(scores):
    # Returns the average of a list
    if len(scores) == 0:
        return 0
    return sum(scores) / len(scores)

def letter_grade(average):
    # Returns a letter grade string
    if average >= 90: return "A"
    elif average >= 75: return "B"
    elif average >= 60: return "C"
    else: return "Fail"

# Call with different inputs
class_a = [88, 76, 95]
class_b = [55, 62, 48]
avg_a = calculate_average(class_a) # 86.3
avg_b = calculate_average(class_b) # 55.0
print(letter_grade(avg_a)) # B
print(letter_grade(avg_b)) # Fail

Java Methods (Functions)

public static double calculateAverage(int[] scores) {
    if (scores.length == 0) return 0;
    int total = 0;
    for (int s : scores) total += s;
    return (double) total / scores.length;
}

public static String letterGrade(double avg) {
    if (avg >= 90) return "A";
    else if (avg >= 75) return "B";
    else if (avg >= 60) return "C";
    else return "Fail";
}

B2.3.4_2 Modularization and scope (local vs global)

Modularization

  • Breaking a program into smaller, self-contained modules (functions, classes, files)
  • Each module has a single, clear responsibility
  • Modules can be reused across different programs
  • Makes code easier to read, test, maintain, and extend

Local Scope

  • Variables declared inside a function exist only for that function's duration
  • Cannot be accessed from outside the function
  • Prevents unintended modification from other parts of the program
def greet():
    message = "Hello" # local
    print(message)
greet()
# print(message) → NameError!

Global Scope

  • Variables declared outside all functions are accessible everywhere
  • Should be used sparingly — global state makes programs harder to debug
  • In Python, use the global keyword to modify a global variable inside a function
total = 0 # global variable
def add_score(score):
    global total
    total += score
add_score(85)
add_score(90)
print(total) # Output: 175

Benefits of Modularization

  • Reusability: write once, use in many places
  • Maintainability: change one function without affecting others
  • Testability: test each function in isolation
  • Readability: meaningful function names make code self-documenting
  • Collaboration: team members can work on separate modules

Modularized Program Example

# Modularized student grade report program

def get_grades():
    # Module 1: data input
    return [88, 72, 95, 64, 80]

def calculate_average(grades):
    # Module 2: calculation
    return sum(grades) / len(grades)

def generate_report(grades, average):
    # Module 3: output
    print(f"Grades: {grades}")
    print(f"Average: {average:.1f}")
    print(f"Result: {'Pass' if average >= 60 else 'Fail'}")

# Main program — calls each module in sequence
grades = get_grades()
avg = calculate_average(grades)
generate_report(grades, avg)
# Output: Grades: [88,72,95,64,80] Average: 79.8 Result: Pass

B2.4.1 Describe algorithm efficiency using Big O notation (AO2)

B2.4.1_1 Time and space complexity

Time Complexity

  • Measures how execution time grows as the input size (n) increases
  • Describes the worst-case number of operations
  • Not the actual time in seconds — it describes the rate of growth
  • Example: an algorithm that checks every item in a list of n items takes time proportional to n

Space Complexity

  • Measures how memory usage grows as input size increases
  • Includes variables, data structures, and call stack
  • A sorting algorithm that creates a copy of the input uses O(n) extra space
  • An in-place algorithm that uses only a few variables uses O(1) extra space

B2.4.1_2 Calculating Big O — common complexities

What is Big O?

  • Big O notation expresses the upper bound of an algorithm's growth rate
  • Constants and lower-order terms are dropped: 3n + 5 → O(n)
  • Focus is on how the algorithm scales as n becomes very large
Big O Name Description Example
O(1) Constant Same time regardless of input size Access list[0]
O(log n) Logarithmic Halves the problem each step Binary search
O(n) Linear One operation per element Linear search
O(n log n) Linearithmic Efficient sorting Merge sort, quicksort
O(n²) Quadratic Nested loops over entire input Bubble sort, selection sort
O(2ⁿ) Exponential Doubles with each added element Brute-force subset enumeration

O(1) — Constant

def get_first(lst):
    return lst[0] # Always 1 operation

O(n) — Linear

def find_max(lst):
    max_val = lst[0]
    for x in lst: # n iterations
        if x > max_val: max_val = x
    return max_val

O(n²) — Quadratic

def all_pairs(lst):
    for i in range(len(lst)): # n
        for j in range(len(lst)): # × n
            print(lst[i], lst[j])

O(log n) — Logarithmic

def binary_search(lst, target):
    low, high = 0, len(lst) - 1
    while low <= high: # halves each time
        mid = (low + high) // 2
        if lst[mid] == target: return mid
        elif lst[mid] < target: low = mid + 1
        else: high = mid - 1

B2.4.1_3 Algorithm choice based on scalability

Choosing the Right Algorithm

  • For small datasets, a simple O(n²) algorithm is acceptable and often easier to implement
  • For large datasets, an O(n log n) or O(log n) algorithm is essential — an O(n²) algorithm on 1,000,000 items would require 10¹² operations
  • Always consider both time and space — a faster algorithm may use significantly more memory
nO(log n)O(n)O(n log n)O(n²)
1031033100
100710066410,000
1,000101,00010,0001,000,000
1,000,000201,000,00020,000,00010¹²

B2.4.2 Construct and trace linear and binary search (AO3)

B2.4.2_1 Linear Search — O(n)

How it Works

  • Checks each element one by one from start to end
  • Works on unsorted or sorted data
  • Best case: target is first element — O(1)
  • Worst case: target is last or not present — O(n)

Python Implementation

def linear_search(lst, target):
    for i in range(len(lst)):
        if lst[i] == target:
            return i # Found: return index
    return -1 # Not found

phones = ["Alice", "Bob", "Carol", "David"]
print(linear_search(phones, "Carol")) # 2
print(linear_search(phones, "Eve")) # -1

Trace for linear_search(["Alice","Bob","Carol","David"], "Carol")

ilst[i]== "Carol"?Action
0"Alice"FalseContinue
1"Bob"FalseContinue
2"Carol"TrueReturn 2

B2.4.2_2 Binary Search — O(log n)

How it Works

  • Requires a sorted list
  • Compares target to the middle element
  • If target is smaller → search the left half
  • If target is larger → search the right half
  • Halves the search space each step → O(log n)

Python Implementation

def binary_search(lst, target):
    low = 0
    high = len(lst) - 1
    while low <= high:
        mid = (low + high) // 2
        if lst[mid] == target:
            return mid
        elif lst[mid] < target:
            low = mid + 1
        else:
            high = mid - 1
    return -1

Trace for binary_search([10, 23, 37, 45, 58, 72, 89], 58)

Steplowhighmidlst[mid]Action
10634545 < 58 → low = 4
24657272 > 58 → high = 4
344458Found! Return 4
  • Only 3 comparisons to find the target in 7 elements — vs up to 7 for linear search

B2.4.2_3 Comparing search efficiency and use cases

Aspect Linear Search Binary Search
Time Complexity O(n) O(log n)
Requires Sorted Data? No Yes
Best Case O(1) — target is first element O(1) — target is middle element
Worst Case O(n) — target not present O(log n) — target not present
Use Case Unsorted list; small datasets; searching by phone number to find a name Large sorted dataset; searching a sorted/indexed name list to find a phone number

B2.4.3 Construct and trace bubble sort and selection sort (AO3)

B2.4.3_1 Bubble Sort

How it Works

  • Repeatedly compares adjacent pairs and swaps them if out of order
  • Each pass "bubbles" the largest unsorted element to its correct position
  • Continues until no swaps are made in a full pass (sorted)
  • Time: O(n²) worst/average; O(n) best (already sorted with optimisation)
  • Space: O(1) — in-place, only needs one temporary variable

Python Implementation

def bubble_sort(lst):
    n = len(lst)
    for i in range(n):
        swapped = False
        for j in range(0, n - i - 1):
            if lst[j] > lst[j + 1]:
                lst[j], lst[j+1] = lst[j+1], lst[j]
                swapped = True
        if not swapped: break # Early exit if sorted
    return lst

data = [64, 25, 12, 22, 11]
print(bubble_sort(data)) # [11,12,22,25,64]

Trace for [64, 25, 12, 22, 11]

PassAfter PassSwaps?
1[25, 12, 22, 11, 64]Yes
2[12, 22, 11, 25, 64]Yes
3[12, 11, 22, 25, 64]Yes
4[11, 12, 22, 25, 64]Yes
5[11, 12, 22, 25, 64]No → stop
  • Bold = element placed in final sorted position that pass

B2.4.3_2 Selection Sort

How it Works

  • Divides list into a sorted left portion and an unsorted right portion
  • Each pass finds the minimum in the unsorted portion and swaps it to the end of the sorted portion
  • Time: always O(n²) — no early exit like bubble sort
  • Space: O(1) — in-place
  • Makes fewer swaps than bubble sort (at most n−1 swaps)

Python Implementation

def selection_sort(lst):
    n = len(lst)
    for i in range(n):
        # Find minimum in unsorted portion
        min_idx = i
        for j in range(i + 1, n):
            if lst[j] < lst[min_idx]:
                min_idx = j
        # Swap minimum into sorted position
        lst[i], lst[min_idx] = lst[min_idx], lst[i]
    return lst

data = [64, 25, 12, 22, 11]
print(selection_sort(data)) # [11,12,22,25,64]

Trace for [64, 25, 12, 22, 11]

Pass (i)Min FoundSwapAfter Pass
011 (idx 4)64 ↔ 11[11, 25, 12, 22, 64]
112 (idx 2)25 ↔ 12[11, 12, 25, 22, 64]
222 (idx 3)25 ↔ 22[11, 12, 22, 25, 64]
325 (idx 3)No swap[11, 12, 22, 25, 64]
464 (idx 4)No swap[11, 12, 22, 25, 64]
  • Bold = element confirmed in its final sorted position

B2.4.3_3 Comparing bubble sort and selection sort

Aspect Bubble Sort Selection Sort
Time (worst/avg) O(n²) O(n²)
Time (best) O(n) with optimisation (already sorted) O(n²) — always scans full unsorted portion
Space O(1) O(1)
Number of Swaps Can be high — many adjacent swaps At most n−1 — one swap per pass
Stable? Yes — equal elements preserve order No — swaps can change relative order
Best Used When Data is nearly sorted; small datasets Minimising writes/swaps is important; small datasets

Summary

  • Both algorithms are simple to implement but inefficient for large datasets — O(n²) becomes impractical for n > 10,000
  • For real-world large-scale sorting, prefer merge sort or quicksort — both O(n log n)
  • These algorithms are valuable for learning and understanding how sorting works at a low level

B2.4.4 Explain recursion and its applications (AO2) HL

B2.4.4_1 Fundamentals — advantages and limitations

What is Recursion?

  • A function that calls itself to solve a smaller version of the same problem
  • Every recursive solution has two essential components:
    • Base case: a condition that stops the recursion (returns a direct answer)
    • Recursive case: reduces the problem towards the base case and calls the function again
  • Each call is placed on the call stack and resolved when the base case is reached

Advantages

  • Elegant and concise — recursive solutions are often far shorter than iterative equivalents
  • Natural fit for problems with a recursive structure (trees, fractals, divide-and-conquer)
  • Improves code readability when the problem itself is recursive in nature
  • Simplifies complex algorithms like quicksort and tree traversal

Limitations

  • Memory usage: each recursive call adds a stack frame — deep recursion can cause a stack overflow
  • Performance: function call overhead makes recursion slower than iteration for simple repetitions
  • Complexity: tracing and debugging recursive code can be harder to follow
  • Risk of infinite recursion if base case is missing or unreachable

B2.4.4_2 Applications — problems suited to recursion

Quicksort

  • Selects a pivot element and partitions the list into elements smaller and larger than the pivot
  • Recursively sorts each partition
  • Average time: O(n log n); worst case O(n²)
def quicksort(lst):
    if len(lst) <= 1: return lst # Base case
    pivot = lst[len(lst) // 2]
    left = [x for x in lst if x < pivot]
    mid = [x for x in lst if x == pivot]
    right = [x for x in lst if x > pivot]
    return quicksort(left) + mid + quicksort(right)

Binary Tree Traversal

  • Trees are naturally recursive: each node has left and right subtrees
  • In-order traversal visits left → root → right to produce sorted output
  • Recursion visits every node exactly once: O(n)
def inorder(node):
    if node is None: return # Base case
    inorder(node.left) # Visit left subtree
    print(node.value) # Visit root
    inorder(node.right) # Visit right subtree

Fractal Image Creation

  • Fractals are shapes that contain smaller copies of themselves
  • Each recursive call draws a smaller version of the same pattern
  • Example: The Sierpiński triangle — a triangle made of three smaller triangles, each containing three smaller triangles, and so on
  • Recursion depth controls level of detail

When to Choose Recursion

  • Use recursion when the problem naturally decomposes into identical sub-problems (trees, divide-and-conquer, fractals)
  • Avoid recursion when depth could be large (risk of stack overflow) or when a simple loop will do
  • Use memoisation (caching results) to avoid recalculating the same sub-problems in recursive algorithms

B2.4.4_3 Limitations — complexity and memory usage

Stack Overflow Example

def countdown(n):
    # Missing base case → infinite recursion → RecursionError
    print(n)
    return countdown(n - 1) # Never stops

def countdown_fixed(n):
    if n < 0: return # Base case prevents overflow
    print(n)
    countdown_fixed(n - 1)

Naive Recursion vs Memoisation

# Naive Fibonacci: O(2ⁿ) — recalculates the same values repeatedly
def fib(n):
    if n <= 1: return n
    return fib(n - 1) + fib(n - 2) # Exponential time!

# With memoisation: O(n) — each value computed only once
memo = {}
def fib_memo(n):
    if n <= 1: return n
    if n in memo: return memo[n] # Use cached result
    memo[n] = fib_memo(n-1) + fib_memo(n-2)
    return memo[n]
  • fib(50) with naive recursion: over 10¹⁰ calls; with memoisation: just 50 unique calls

B2.4.5 Construct, trace recursive algorithms (AO3) HL

B2.4.5_1 Simple, non-branching recursive algorithms

Constructing Recursive Algorithms

  • Solve problems by calling themselves with smaller inputs
  • Consist of:
    • Base case: Terminates recursion
    • Recursive case: Reduces problem size
  • Non-branching: Makes a single recursive call per iteration

Factorial Example

def factorial(n):
    # Base case
    if n == 0 or n == 1:
        return 1
    # Recursive case
    return n * factorial(n - 1)
# Computes n! (e.g., 5! = 5 × 4 × 3 × 2 × 1 = 120)

Tracing Recursive Algorithms

  • Follow each recursive call, tracking values until base case
  • Then compute final output by unwinding the call stack
Trace for factorial(4):
  • Call 1: factorial(4) → 4 × factorial(3)
  • Call 2: factorial(3) → 3 × factorial(2)
  • Call 3: factorial(2) → 2 × factorial(1)
  • Call 4: factorial(1) → Base case, returns 1
  • Unwind:
    • factorial(2) → 2 × 1 = 2
    • factorial(3) → 3 × 2 = 6
    • factorial(4) → 4 × 6 = 24
  • Result: factorial(4) returns 24

Sum of Numbers Example

def sum_to_n(n):
    # Base case
    if n <= 1:
        return n
    # Recursive case
    return n + sum_to_n(n - 1)
# Computes sum from 1 to n (e.g., sum_to_n(4) = 1+2+3+4 = 10)
Trace for sum_to_n(4):
  • Call 1: sum_to_n(4) → 4 + sum_to_n(3)
  • Call 2: sum_to_n(3) → 3 + sum_to_n(2)
  • Call 3: sum_to_n(2) → 2 + sum_to_n(1)
  • Call 4: sum_to_n(1) → Base case, returns 1
  • Unwind:
    • sum_to_n(2) → 2 + 1 = 3
    • sum_to_n(3) → 3 + 3 = 6
    • sum_to_n(4) → 4 + 6 = 10
  • Result: sum_to_n(4) returns 10

Key Considerations

  • Ensure a clear base case to prevent infinite recursion
  • Non-branching algorithms are simpler to trace and construct
  • Example Use Case: Calculating power of a number (x^n)
def power(x, n):
    if n == 0: # Base case
        return 1
    return x * power(x, n - 1) # Recursive case
# Trace for power(2, 3):
# power(2, 3) → 2 * power(2, 2)
# power(2, 2) → 2 * power(2, 1)
# power(2, 1) → 2 * power(2, 0)
# power(2, 0) → 1
# Unwind: 2 * 1 = 2, 2 * 2 = 4, 2 * 4 = 8
# Result: 8

B2.5.1 Construct file-processing code (AO3)

B2.5.1_1 Manipulate text files

Description

  • Writing code to read from, write to, or modify text files
  • Enables data persistence and processing
  • Purpose: Store, retrieve, or update data for tasks like logging, configuration, or analysis
  • Example: Reading student names from a file, updating grades, and saving results

B2.5.1_2 Open files in read, write, append modes

Read Mode ('r')

  • Opens file for reading
  • Raises error if file doesn't exist
  • Example: Reading student grades from text file

Write Mode ('w')

  • Opens file for writing
  • Overwrites if exists or creates new file
  • Example: Writing processed data to a file

Append Mode ('a')

  • Opens file to add data at the end
  • Preserves existing content
  • Example: Adding new log entries without overwriting

Python Code Example

# Example: Manipulating a text file with different modes
# Write mode: Create or overwrite a file
with open('students.txt', 'w') as file:
    file.write("Alice,85\nBob,90\n") # Write initial data
# Append mode: Add new data
with open('students.txt', 'a') as file:
    file.write("Charlie,78\n") # Append new student
# Read mode: Read and print file contents
with open('students.txt', 'r') as file:
    content = file.read()
    print("File Contents:", content)

B2.5.1_3 Read, write, append, close files

Read

  • Retrieves data using methods like read(), readline(), or readlines()
  • Example: file.read() reads entire file as string
  • file.readlines() reads lines into a list

Write

  • Writes data to file in 'w' mode
  • Overwrites existing content
  • Example: file.write("data") writes a string

Append

  • Adds data to end in 'a' mode
  • Preserves existing content
  • Example: file.write("new data\n") appends a line

Close

  • Closes file to free system resources
  • Automatically handled using 'with' statement
  • Example: with open('file.txt', 'r') as file: ensures closure

Python Code Example

# Read, write, append example
# Write initial data
with open('grades.txt', 'w') as file:
    file.write("Alice: 85\n")
# Append additional data
with open('grades.txt', 'a') as file:
    file.write("Bob: 90\n")
# Read and process file
with open('grades.txt', 'r') as file:
    lines = file.readlines() # Read all lines into a list
    for line in lines:
        name, grade = line.strip().split(': ')
        grade = int(grade) + 5 # Increase grade by 5
        print(f"Updated: {name}, {grade}")

B2.5.1_4 Java classes: Scanner, FileWriter, BufferedReader

Scanner

  • Java class (java.util.Scanner) for reading data
  • Parses input into tokens
import java.util.Scanner;
import java.io.File;
File file = new File("students.txt");
Scanner scanner = new Scanner(file);
while (scanner.hasNextLine()) {
    System.out.println(scanner.nextLine());
}
scanner.close();

FileWriter

  • Java class (java.io.FileWriter) for writing
  • Writes character data to files
import java.io.FileWriter;
FileWriter writer = new FileWriter("students.txt");
writer.write("Alice,85\n");
writer.close(); // Explicitly close

BufferedReader

  • Java class for efficient reading
  • Buffers input for better performance
import java.io.BufferedReader;
import java.io.FileReader;
BufferedReader reader = new BufferedReader(
    new FileReader("students.txt"));
String line;
while ((line = reader.readLine()) != null) {
    System.out.println(line);
}
reader.close();

B2.5.1_5 Python functions: open(), read(), readline(), write(), close()

open()

  • Opens file in specified mode
  • Returns a file object
  • Example: file = open('data.txt', 'r')

read()

  • Reads entire file as string
  • Example: content = file.read()

readline()

  • Reads a single line
  • Returns empty string at end
  • Example: line = file.readline()

write()

  • Writes string to file
  • Example: file.write("Hello\n")

close()

  • Closes file, freeing resources
  • Used without 'with' statement
  • Example: file.close()

Example Program

# Process a CSV file with student data
# Write student data
with open('students.csv', 'w') as file:
    file.write("Name,Grade\nAlice,85\nBob,90\n")
# Read and manipulate data
with open('students.csv', 'r') as file:
    lines = file.readlines()[1:] # Skip header
    for line in lines:
        name, grade = line.strip().split(',')
        grade = int(grade) + 5 # Increase grade by 5
        print(f"Updated: {name}, {grade}")
# Append new student
with open('students.csv', 'a') as file:
    file.write("Charlie,78\n")

Use Case

  • A program managing a log file:
    • Writes new entries (write, append mode)
    • Reads and analyzes past entries (readlines)
    • Ensures proper closure (with or close)
  • Enables reliable file processing for tasks like tracking user activity or storing configuration data