Programming fundamentals
This unit moves from writing small Python programs to organising larger ones. It compares the functional and object-oriented programming paradigms, then uses Python's data structures (lists, tuples, sets, dictionaries and nested lists), files, GUIs with Tkinter and Pygame, and SQLite databases to solve real problems. It ends with the techniques that make programs correct: debugging with print statements, breakpoints and watchpoints, and unit testing.
Learning outcomes (4)
- Understand and evaluate applications of various programming paradigms.
- Use more advanced programming constructs such as data structures (lists and so on), file handling (disk I/O to write to storage) and databases in Python.
- Implement complex algorithms that use lists and other data structures in Python.
- Use more advanced techniques (unit tests, breakpoints, watches) for testing and debugging code in Python.
3.1Programming paradigmOften examined
A programming paradigm is a style or method of writing and organising program code. It decides how the code is structured, how control flows from one statement to the next, and how data is managed. In other words, it is a particular methodology for using a programming language to solve a problem.
A programmer may create his own functions and structures inside a language, but these are not a paradigm. The paradigm comes from the language itself: the language gives guidelines, tools and features that tell the programmer what to build and how to build it. For example, Python supports both the functional and the object-oriented paradigms, while SQL follows the declarative (query) style.
Why paradigms are important
- Complexity reduction: a paradigm breaks a big, complex problem into smaller parts (functions or objects) that are easier to understand and solve.
- Structure: code, program flow and data are organised in a standard, predictable way, so the program is easier to read, test and maintain.
- Standard tools: each paradigm comes with well-known techniques that many programmers already use, so a team can work together more easily.
- Focus: the developer concentrates on the task in hand instead of reinventing a way to organise the program.
| Paradigm | Main idea | Unit of the program | Example languages |
|---|---|---|---|
| Procedural (imperative) | A sequence of steps that change variables | Procedure / block of statements | C, Pascal, Python |
| Functional | Solve by calling functions; avoid changing state | Function | Haskell, Lisp, Python |
| Object-oriented | Model the problem as objects that hold data and behaviour | Class and object | Java, C++, Python |
| Declarative (query) | State what result is wanted, not how | Query / rule | SQL |
3.1.1Functional programmingOften examined
Functional programming is a paradigm in which the program is divided into functions according to their functionality. The solution of the problem is broken into segments and each segment is handled by a function. Every step of the solution is mapped to a group of functions.
In functional programming the developer focuses on what is to be achieved rather than how to achieve it. Functions take inputs and return outputs and, ideally, do not change any value outside themselves (they have no side effects and do not change the program's state). This is why the MCQ phrase "uses functions without changing state" means functional programming.
- Pure function
- A function whose result depends only on its inputs and which does not change any variable outside itself. Calling it twice with the same input always gives the same output.
- Higher-order function
- A function that takes another function as an argument or returns a function, for example Python's and .
- Lambda
- A small nameless function written in one line, such as .
| Pros | Cons |
|---|---|
| Simpler code. | Not easy for beginners; needs practice. |
| Code is easier to track for logic and to debug. | May involve overhead, especially when handling large data structures. |
| Functions are easier to test and run. | Complex algorithms may be harder to express, often requiring higher-order functions. |
| Naturally supports parallel and concurrent programming. | Requires careful handling of data sharing and synchronisation for efficient execution. |
| Emphasises what to compute rather than how, hence more concise code. | May be less efficient in performance-critical scenarios. |
If the flow of the solution is clear, functional programming makes the work easier. Updating the code for any change is simple, because only the particular function that is affected needs to be modified; the whole program does not need to be read and understood again.
# Functional style: small pure functions, no global state is changed
def square(n):
return n * n
def is_even(n):
return n % 2 == 0
marks = [3, 8, 5, 10, 6]
squares = list(map(square, marks)) # apply square() to every item
evens = list(filter(is_even, marks)) # keep only the even items
total = sum(map(lambda n: n * 2, marks)) # lambda = small nameless function
print("Original list:", marks)
print("Squares:", squares)
print("Even marks:", evens)
print("Sum of doubled marks:", total)Original list: [3, 8, 5, 10, 6]
Squares: [9, 64, 25, 100, 36]
Even marks: [8, 10, 6]
Sum of doubled marks: 64- Lines 2 to 6 define two pure functions. Each one only returns a value; neither changes the list.
- Line 10: applies to every element; collects the results.
- Line 11: keeps only the elements for which returns True.
- Line 12: a lambda doubles each mark and adds them: .
- The original list is unchanged at the end, which is the functional idea of not changing state.
For comparison, the same total written in the ordinary step-by-step (imperative) style changes the variable in every pass of the loop:
# Imperative (procedural) style: a variable is changed step by step
marks = [3, 8, 5, 10, 6]
total = 0
for m in marks:
total = total + m * 2 # state (total) changes in every pass
print("Sum of doubled marks:", total)Sum of doubled marks: 643.1.2Object oriented programmingOften examined
Object oriented programming (OOP) is a software development paradigm based on data and objects. An object contains attributes (data that describe its properties) and methods (functions that define its behaviour). Programs are built around objects that copy real-world things, such as a car, a fruit or a bank account, so developers understand the problem more naturally.
The main advantage of OOP is that the emphasis in problem solving is on objects and not on the logic. OOP is implemented through classes: first a class defines the object and its functions, and later actual instances (objects) are created when they are needed in the program. OOP limits direct access to data, which gives safety and reliability of data and programs. It is very useful for complex systems in which developers map real objects and their relationships.
| Pros | Cons |
|---|---|
| Simplifies complex systems by breaking them into smaller reusable components. | May result in complex designs if not implemented thoughtfully. |
| Hides implementation details, hence improving code maintainability and security. | At times code is harder to understand and debug due to encapsulation. |
| Establishes hierarchical relationships between classes. | Rigid design due to inheritance may reduce flexibility. |
| Objects of different types are treated as if they were of the same type. | Polymorphism increases complexity, making code harder to understand and reason about. |
| Represents real-world objects and their relationships effectively. | May not be well-suited for certain problems, such as computationally intensive tasks. |
Key concepts: class and object
- Class
- An object creation stencil or template. It specifies the attributes (variables or properties) and methods (functions) that its objects will hold.
- Object
- An instance of a class that denotes a real thing, such as a person, a car or a bank account. Every object has its own attribute values but uses the methods defined in the class.
- Instance
- Creating an object from a class is called instantiation; the object is an instance of that class.
A class is like the map of a house; each house (object) built from that map has particular properties such as size, colour or number of rooms. All objects share the basic structure defined by the class but have their own values. In the book's example the class is the stencil for every stationery item in a bookshop:
| Class: StationaryItem | Object 1: a blue XYZ pen | Object 2: a red ABC pencil |
|---|---|---|
| name | "Pen" | "Pencil" |
| price | 200 | 100 |
| color | "Blue" | "Red" |
| brand | "XYZ" | "ABC" |
| methods | , | same methods |
class StationaryItem:
def __init__(self, name, price, color, brand): # constructor: runs when an object is made
self.name = name # attributes (data)
self.price = price
self.color = color
self.brand = brand
def get_price(self): # method (behaviour)
return self.price
def set_price(self, new_price):
self.price = new_price
pen = StationaryItem("Pen", 200, "Blue", "XYZ") # object 1
pencil = StationaryItem("Pencil", 100, "Red", "ABC") # object 2
print(pen.name, pen.color, pen.get_price())
print(pencil.name, pencil.color, pencil.get_price())
pencil.set_price(120)
print("New price of pencil:", pencil.get_price())Pen Blue 200
Pencil Red 100
New price of pencil: 120- Line 1: starts the definition of the template.
- Line 2: is the constructor. It runs automatically when an object is created and stores the four attribute values. means "this object".
- Lines 8 to 12: two methods read and change the price, so other code does not touch the attribute directly.
- Lines 14 and 15 create two objects (instances) from the same class.
- Line 19 changes only the pencil's price; the pen is not affected because each object keeps its own data.
Encapsulation
Encapsulation is information hiding: the data (attributes) and the methods that operate on the data are combined into a single unit, the class. Direct access to some components is not allowed; they are reached only through designated methods. The internal working of the object stays hidden. Encapsulation protects data from unwanted access or changes and makes code modular and easier to read.
In a bank, the holder's name, account number and balance are critical. Instead of changing the balance directly, adds to it, subtracts from it and reads it without changing it.
class BankAccount:
def __init__(self, holder, balance):
self.holder = holder
self.__balance = balance # two underscores: hidden (private) attribute
def deposit(self, amount):
if amount > 0:
self.__balance += amount
def withdraw(self, amount):
if amount <= self.__balance:
self.__balance -= amount
else:
print("Insufficient balance")
def get_balance(self):
return self.__balance
acc = BankAccount("Ayesha", 5000)
acc.deposit(1500)
acc.withdraw(10000)
acc.withdraw(2000)
print("Balance:", acc.get_balance())
try:
print(acc.__balance) # direct access is blocked
except AttributeError:
print("Direct access to __balance is not allowed")Insufficient balance
Balance: 4500
Direct access to __balance is not allowedThe name (two leading underscores) makes the attribute private: code outside the class cannot read it as . The withdrawal of 10000 is refused by the method's check, which protects the data. Final balance: .
Abstraction
Abstraction shows only the necessary information and hides the background details or implementation. A driver uses start, stop and accelerate, but how the engine burns fuel and how gears shift are hidden. In code, the user of a class only needs to know the names of its methods, not how they are written.
Inheritance
Inheritance lets you create new classes (subclasses or derived classes) from existing parent classes. A subclass inherits the attributes and methods of its parent, which reduces repetition of code and extends existing structures efficiently. A has speed, colour, wheels and the operations and . A is a type of vehicle, so it inherits all these and adds its own, such as and .
class Vehicle: # parent (base) class
def __init__(self, color, wheels):
self.color = color
self.wheels = wheels
self.speed = 0
def accelerate(self, amount):
self.speed += amount
def brake(self):
self.speed = 0
class Car(Vehicle): # child (derived) class inherits Vehicle
def __init__(self, color, seating_capacity):
super().__init__(color, 4) # reuse the parent constructor
self.seating_capacity = seating_capacity
def open_sunroof(self): # extra behaviour only a Car has
return "Sunroof opened"
c = Car("White", 5)
c.accelerate(60) # inherited method
print("Colour:", c.color, "| Wheels:", c.wheels, "| Speed:", c.speed)
print("Seats:", c.seating_capacity, "|", c.open_sunroof())
c.brake()
print("Speed after brake:", c.speed)Colour: White | Wheels: 4 | Speed: 60
Seats: 5 | Sunroof opened
Speed after brake: 0Writing makes Car a child of Vehicle. calls the parent constructor, so colour, wheels and speed are set without rewriting code. and are used by the Car object although they are written only in Vehicle.
Polymorphism
Polymorphism ("many forms") allows objects of different classes to be treated as instances of a common type, although each class implements the common operation in its own way. It allows generic code that works with many kinds of objects. A triangle, a rectangle and a circle are all shapes, but the area of a triangle needs base and height, a rectangle needs length and width, and a circle needs the radius. Instead of three differently named methods, each class has an method and the same instruction gives the correct result for each object.
import math
class Shape:
def area(self):
return 0
class Triangle(Shape):
def __init__(self, base, height):
self.base, self.height = base, height
def area(self): # same method name, own formula
return 0.5 * self.base * self.height
class Rectangle(Shape):
def __init__(self, length, width):
self.length, self.width = length, width
def area(self):
return self.length * self.width
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return round(math.pi * self.radius ** 2, 2)
shapes = [Triangle(6, 4), Rectangle(5, 3), Circle(2)]
for s in shapes: # one instruction, three behaviours
print(type(s).__name__, "area =", s.area())Triangle area = 12.0
Rectangle area = 15
Circle area = 12.57The loop does not check the type of . Python calls the that belongs to each object's own class: , and .
3.2Python building blocks (revision)
Unit 3 uses Python constructs from Grade 11 in every program. This short revision collects them so that the programs in the rest of the unit are easy to follow.
Data types and operators
| Type | Example | Mutable? |
|---|---|---|
| int | no | |
| float | no | |
| str | no | |
| bool | no | |
| list | yes | |
| tuple | no | |
| set | yes | |
| dict | yes |
| Operator group | Operators | Example and result |
|---|---|---|
| Arithmetic | gives 3, gives 1 | |
| Relational | gives True | |
| Logical | and, or, not | gives False |
| Assignment | adds 1 to x | |
| Membership | in, not in | gives True |
age = 17 # int: whole number
height = 5.6 # float: number with a decimal point
name = "Hina" # str: text
passed = True # bool: True or False
print(type(age), type(height), type(name), type(passed))
print(7 / 2, 7 // 2, 7 % 2, 2 ** 5) # division, floor division, remainder, power
print(10 > 3 and 2 > 5, not passed) # logical operators
print("Py" + "thon", "ab" * 3) # string joining and repetition<class 'int'> <class 'float'> <class 'str'> <class 'bool'>
3.5 3 1 32
False False
Python abababControl structures
Selection (, , ) chooses one path. Repetition (, ) repeats statements. leaves a loop and skips to the next pass. Python marks a block by indentation (4 spaces), not by braces.
marks = [45, 78, 91, 33]
for m in marks: # for loop: repeats once for each item
if m >= 80: # if-elif-else: choose one path
grade = "A"
elif m >= 50:
grade = "B"
elif m >= 40:
grade = "C"
else:
grade = "Fail"
print(m, grade)
n = 1
while n <= 16: # while loop: repeats while the condition is True
print(n, end=" ")
n = n * 2
print()
for i in range(1, 10):
if i == 4:
continue # skip the rest of this pass
if i == 7:
break # leave the loop completely
print(i, end=" ")45 C
78 B
91 A
33 Fail
1 2 4 8 16
1 2 3 5 6 Functions
A function is a named block of code that does one job. It is defined with , receives values through parameters, and sends a result back with . Functions let us reuse code and split a big problem into small parts, which is the basis of complexity reduction.
def area_of_rectangle(length, width=1): # width has a default value
"""Return the area of a rectangle."""
return length * width
def describe(n):
if n % 2 == 0:
return "even"
return "odd"
a = area_of_rectangle(4, 5) # arguments 4 and 5 go to length and width
b = area_of_rectangle(7) # width uses its default value 1
print(a, b)
print(9, "is", describe(9))20 7
9 is oddStrings
A string is a sequence of characters. Characters are numbered from index 0; a negative index counts from the end. Strings are immutable, so methods such as return a new string.
s = "Programming Fundamentals"
print(len(s)) # number of characters
print(s[0], s[-1]) # first and last character (index starts at 0)
print(s[0:11]) # slicing: characters 0 to 10
print(s.upper())
print(s.lower().count("a"))
print(s.split()) # break into a list of words
print(s.replace("Fundamentals", "Basics"))
print("gram" in s)24
P s
Programming
PROGRAMMING FUNDAMENTALS
3
['Programming', 'Fundamentals']
Programming Basics
True3.2.1ListOften examined
A list is a basic data structure in Python that holds a group of elements in a sequence. It is written in square brackets, for example . Lists are flexible: one list may hold elements of different data types (integers, strings, booleans and so on).
- Ordered: the order in which items are added is kept. Index 0 holds the first item, index 1 the second, and so on. This lets us retrieve an item by its position.
- Mutable and dynamic: items can be added, changed or removed at any time, so the size of a list changes as needed.
- Duplicates allowed: the same value may appear more than once.
| Function / method | What it does | Example |
|---|---|---|
| puts x at index i and shifts later items right | ||
| adds x at the end | ||
| deletes the first item equal to x (error if x is absent) | ||
| deletes and returns the item at index i (last item if i is omitted) | ||
| , | number of items, total of the items | |
| True if x is in the list |
student_grades = [85, 92, 78, 95, 82]
student_grades.insert(2, 90)
print("Grades after adding a new grade:", student_grades)
student_grades.append(100)
print("Grades after adding another new grade:", student_grades)
student_grades.remove(78)
print("Grades after removing a grade:", student_grades)
lowest_grade = student_grades.pop(2)
print("Grades after removing the grade on index 2:", student_grades)
if 88 in student_grades:
student_grades.remove(88)
else:
print("Grade 88 not found in the list")
average_grade = sum(student_grades) / len(student_grades)
print("Average grade:", {average_grade})Grades after adding a new grade: [85, 92, 90, 78, 95, 82]
Grades after adding another new grade: [85, 92, 90, 78, 95, 82, 100]
Grades after removing a grade: [85, 92, 90, 95, 82, 100]
Grades after removing the grade on index 2: [85, 92, 95, 82, 100]
Grade 88 not found in the list
Average grade: {90.8}Line-by-line explanation
- Line 1 creates the list with 5 elements.
- Line 3: places 90 at index 2; 78, 95 and 82 move one place right.
- Line 5: adds 100 at the end.
- Line 8: deletes the value 78 (we know the value).
- Line 10: deletes the element at index 2, which is now 90 (we know the position). The removed value is kept in .
- Lines 13 to 16: 88 is removed only if it is in the list. Without this if-else, would stop the program with a ValueError.
- Line 18: and give the average without writing a loop.
student_grades = [85, 92, 78, 95, 82]
student_grades.insert(2, 90)
student_grades.append(100)
print("List before:", student_grades)
odd_numbers = [g for g in student_grades if g % 2 != 0] # find the odd items
print("Odd numbers found:", odd_numbers)
for g in odd_numbers:
student_grades.remove(g) # remove each one
print("List after removing odd numbers:", student_grades)List before: [85, 92, 90, 78, 95, 82, 100]
Odd numbers found: [85, 95]
List after removing odd numbers: [92, 90, 78, 82, 100]The odd numbers are first collected in a separate list (a value is odd when ). They are then removed one by one. Removing items from a list while looping over the same list skips elements, so the separate list is the safe method.
3.2.2Tuples
A tuple is written with round brackets, for example . It is a special type of list with one feature: it is immutable, so its contents cannot be updated once it is created. We can read the items and their arrangement but cannot add, remove or rearrange them. Tuples are used when a set of values must stay in order and unchanged throughout the program, for example the days of the week or geographical coordinates (latitude, longitude).
grades = (85, 92, 78, 92, 85, 95)
print(grades)
print("First grade:", grades[0])
print("Third grade:", grades[2])
if 92 in grades:
print("Student got 92")
print(grades.count(92))
new_grades = grades + (88, 90)
print(new_grades)(85, 92, 78, 92, 85, 95)
First grade: 85
Third grade: 78
Student got 92
2
(85, 92, 78, 92, 85, 95, 88, 90)- Line 1 creates the tuple; line 2 prints it, showing that duplicates (85 and 92) are allowed.
- Lines 3 and 4 access items by index (index starts at 0), so is the third grade, 78.
- Line 5 uses the operator to check whether 92 is present.
- Line 7: returns how many times 92 appears (2).
- Line 8: the operator joins two tuples into a new tuple; the original is not changed.
Trying to change a tuple stops the program with an error:
days = ("Monday", "Tuesday", "Wednesday")
days[0] = "Sunday"Traceback (most recent call last):
File "prog.py", line 2, in <module>
days[0] = "Sunday"
~~~~^^^
TypeError: 'tuple' object does not support item assignment| List | Tuple |
|---|---|
| Square brackets [ ] | Round brackets ( ) |
| Mutable: items can be added, changed, removed | Immutable: cannot be changed after creation |
| Methods such as append(), insert(), remove(), pop() | Only count() and index() |
| Used for data that changes | Used for fixed data (days, coordinates) |
3.2.3Sets
A set is written with curly brackets, for example . It is a collection of unique and unordered elements: a set cannot hold duplicate values, and if the same value is added twice only one copy is kept. Because it is unordered, a set has no index. Sets are used to store unique usernames, product IDs and so on, and the operator checks very quickly whether an element is present.
grades = {85, 92, 78, 92, 85, 95}
print(grades)
grades.add(88)
print(grades)
if 92 in grades:
print("Student has 92 among the grades."){92, 85, 78, 95}
{78, 85, 88, 92, 95}
Student has 92 among the grades.- Line 1: the duplicates 85 and 92 are removed automatically, so the set keeps 4 values.
- Line 3: inserts a new element.
- Line 5: checks whether 92 exists and line 6 prints the message.
Sets also support the mathematical operations union, intersection and difference:
a = {1, 2, 3, 4}
b = {3, 4, 5}
print(a | b) # union: items in either set
print(a & b) # intersection: items in both sets
print(a - b) # difference: items in a but not in b
a.discard(1) # remove an item (no error if missing)
print(a, len(a)){1, 2, 3, 4, 5}
{3, 4}
{1, 2}
{2, 3, 4} 33.2.4DictionaryOften examined
A dictionary stores data as key-value pairs, written as . Each key is linked with one value, just like a phone book where the person's name (key) is linked with the phone number (value). A list is searched by index, while a dictionary is searched by its key.
- Keys must be unique and unchangeable (immutable) types such as strings, integers or tuples. A duplicate key is not created; assigning to an existing key updates its value.
- Values can be any type: text, numbers, lists and even other dictionaries.
- Dictionaries keep the link between keys and values (for example usernames and passwords) and, in Python 3.7 and later, remember the order in which keys were inserted.
| Operation | Syntax |
|---|---|
| Create | |
| Read a value | |
| Add or update | |
| Delete | |
| Check a key | |
| Loop over pairs |
student_grades = {
'Amir': 90,
'Bilal': 85,
'Dilawar': 92
}
for student, grade in student_grades.items():
print(f"{student}: {grade}")
student_grades['Zaid'] = 88
student_grades['Bilal'] = 87
del student_grades['Dilawar']
if 'Dilawar' in student_grades:
print(f"\n Dilawar's grade:", student_grades['Dilawar'], "\n")
else:
print("Student Dilawar not found \n")
for student, grade in student_grades.items():
print(f"{student}: {grade}")Amir: 90
Bilal: 85
Dilawar: 92
Student Dilawar not found
Amir: 90
Bilal: 87
Zaid: 88- Lines 1 to 5 create the dictionary: names are keys and marks are values.
- Lines 7 and 8: gives each key-value pair, which the loop prints.
- Line 10 adds a new key 'Zaid' at the end. Line 11 uses an existing key 'Bilal', so its value is updated from 85 to 87, not duplicated.
- Line 13: removes the key 'Dilawar' and its value.
- Lines 15 to 18 check whether 'Dilawar' is still a key; lines 20 and 21 print the final dictionary for confirmation.
If line 13 is commented out (), Dilawar is not deleted:
Amir: 90
Bilal: 85
Dilawar: 92
Dilawar's grade: 92
Amir: 90
Bilal: 87
Dilawar: 92
Zaid: 88student_test_number = {}
while True:
name = input("Enter student name (or 'q' to quit): ")
if name.lower() == 'q':
break
grade = float(input("Enter Test-Number for {}: ".format(name)))
student_test_number[name] = grade
name = input("Enter student name to view test-number: ")
if name in student_test_number:
print(f"{name}'s test-number: {student_test_number[name]}")
else:
print(f"Student '{name}' not found.")Enter student name (or 'q' to quit): Amir
Enter Test-Number for Amir: 90
Enter student name (or 'q' to quit): Bilal
Enter Test-Number for Bilal: 87
Enter student name (or 'q' to quit): Dilawar
Enter Test-Number for Dilawar: 92
Enter student name (or 'q' to quit): Zaid
Enter Test-Number for Zaid: 88
Enter student name (or 'q' to quit): q
Enter student name to view test-number: Bilal
Bilal's test-number: 87.0- Line 1 creates an empty dictionary. Line 2 starts an endless loop that stops only at .
- Line 4: accepts both 'q' and 'Q' to quit.
- Line 6 converts the typed test number to float (so 87 is stored as 87.0) and line 7 stores it under the student's name.
- Lines 9 to 13 search the dictionary with and print the value or a 'not found' message.
Finding a value in a dictionary versus a list
Searching a dictionary takes much less time than searching a list. To find a value in a list, Python checks the items one by one (linear search), so the time grows with the size of the list. A dictionary uses the key to jump directly to the value (hashing), so the time stays almost the same however large it is. The book tested this with 1 lakh (100 000) random 4-character entries; the program below repeats the experiment.
import random, string, time
# 1 lakh (100000) random 4-letter strings in a list, and the same strings as dictionary keys
words = [''.join(random.choices(string.ascii_lowercase, k=4)) for i in range(100000)]
table = {w: n for n, w in enumerate(words)}
target = words[-1] # worst case for the list: the last item
start = time.perf_counter()
found_in_list = target in words # list: checks items one by one
list_time = (time.perf_counter() - start) * 1000
start = time.perf_counter()
found_in_dict = target in table # dictionary: jumps straight to the key
dict_time = (time.perf_counter() - start) * 1000
print("Searching for:", target, found_in_list, found_in_dict)
print(f"List search time: {list_time:.4f} msec")
print(f"Dictionary search time: {dict_time:.4f} msec")
print("Dictionary is faster:", dict_time < list_time)Searching for: nyss True True
List search time: 0.7784 msec
Dictionary search time: 0.0014 msec
Dictionary is faster: Truestudent_grades = {'Amir': 90, 'Bilal': 85, 'Dilawar': 92}
name = input("Enter student name to search: ")
if name in student_grades:
print(f"{name}'s grade: {student_grades[name]}")
else:
choice = input(f"{name} not found. Add this student? (y/n): ")
if choice.lower() == 'y':
grade = int(input(f"Enter grade for {name}: "))
student_grades[name] = grade
print(f"{name} added.")
for student, grade in student_grades.items():
print(f"{student}: {grade}")Enter student name to search: Sana
Sana not found. Add this student? (y/n): y
Enter grade for Sana: 77
Sana added.
Amir: 90
Bilal: 85
Dilawar: 92
Sana: 773.2.5Nested listOften examined
A nested list is a list of lists: its elements (inner lists or sub-lists) are themselves lists, which may contain further lists. Nested lists are very useful for complex data such as matrices, where each inner list holds one row. The element in row and column is reached with two indexes: .
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
]
print(matrix)
for i in matrix:
print(i)
for row in range(len(matrix)):
for col in range(len(matrix)):
print(matrix[row][col], end=', ')[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
[1, 2, 3]
[4, 5, 6]
[7, 8, 9]
1, 2, 3, 4, 5, 6, 7, 8, 9, - Lines 1 to 5 define the matrix; each inner list is one row.
- Line 6 prints the whole nested list in one line.
- Lines 8 and 9: the loop runs 3 times (once per inner list) and prints one row per line, so the output looks like a matrix.
- Lines 11 to 13: a nested loop. The outer index selects a row and the inner index visits every column of that row. keeps all 9 numbers on one line.
To print the elements in matrix form with the nested loop (the book leaves this as an activity), print a new line after each row:
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
for row in range(len(matrix)):
for col in range(len(matrix[row])):
print(matrix[row][col], end=" ")
print() # new line after each row1 2 3
4 5 6
7 8 9 Transpose of a matrix
The transpose of a matrix swaps rows and columns: the element at row , column moves to row , column . So in the program the indexes of the transpose are reversed: .
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
print('Matrix is: ', matrix)
for i in matrix:
print(i)
trans = [
[0, 0, 0],
[0, 0, 0],
[0, 0, 0]
]
for row in range(len(matrix)):
for col in range(len(matrix)):
trans[col][row] = matrix[row][col]
print('Transpose of the Matrix is: ', trans)
for j in trans:
print(j)Matrix is: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
[1, 2, 3]
[4, 5, 6]
[7, 8, 9]
Transpose of the Matrix is: [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
[1, 4, 7]
[2, 5, 8]
[3, 6, 9]- Lines 10 to 14 create , a matrix of zeros to hold the result.
- Lines 16 and 17: the nested loop visits every element of .
- Line 18 copies each element to the swapped position, so row 1 of the matrix becomes column 1 of the transpose.
- Lines 20 to 22 print the result.
Be careful with the indexes. If they are not swapped, the program only copies the matrix:
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
trans = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
for row in range(len(matrix)):
for col in range(len(matrix)):
trans[row][col] = matrix[row][col] # indices NOT swapped: a copy, not a transpose
print('Transpose of the Matrix is: ', trans)Transpose of the Matrix is: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]| row | col | matrix[row][col] | stored in | trans after step |
|---|---|---|---|---|
| 0 | 0 | 1 | trans[0][0] | [[1,0,0],[0,0,0],[0,0,0]] |
| 0 | 1 | 2 | trans[1][0] | [[1,0,0],[2,0,0],[0,0,0]] |
| 0 | 2 | 3 | trans[2][0] | [[1,0,0],[2,0,0],[3,0,0]] |
| 1 | 0 | 4 | trans[0][1] | [[1,4,0],[2,0,0],[3,0,0]] |
| 1 | 1 | 5 | trans[1][1] | [[1,4,0],[2,5,0],[3,0,0]] |
| 1 | 2 | 6 | trans[2][1] | [[1,4,0],[2,5,0],[3,6,0]] |
| 2 | 0 | 7 | trans[0][2] | [[1,4,7],[2,5,0],[3,6,0]] |
| 2 | 1 | 8 | trans[1][2] | [[1,4,7],[2,5,8],[3,6,0]] |
| 2 | 2 | 9 | trans[2][2] | [[1,4,7],[2,5,8],[3,6,9]] |
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
for row in range(len(matrix)):
for col in range(len(matrix[row])):
if matrix[row][col] % 2 != 0: # odd value: invert the sign
matrix[row][col] = -matrix[row][col]
else: # even value: multiply by 2
matrix[row][col] = matrix[row][col] * 2
for r in matrix:
print(r)[-1, 4, -3]
[8, -5, 12]
[-7, 16, -9]def transpose(m):
n = len(m)
t = [[0] * n for i in range(n)]
for row in range(n):
for col in range(n):
t[col][row] = m[row][col]
return t
def is_symmetric(m):
return m == transpose(m) # symmetric if X equals its transpose
X = [[1, 7, 3],
[7, 4, 5],
[3, 5, 6]]
Y = [[1, 2, 3],
[4, 5, 6],
[7, 8, 9]]
print("X is symmetric:", is_symmetric(X))
print("Y is symmetric:", is_symmetric(Y))X is symmetric: True
Y is symmetric: FalseTwo nested lists are equal () when all their elements are equal in the same positions, so comparing X with its transpose answers the question directly.
3.2.6List as a value in dictionary
A dictionary holds one value per key, but that value can be a list, so several values can be stored under a single key. For example, each student's record can be stored with the student's name as the key and a list holding the registration number and marks as the value.
students = {
"Sara": ["345", 85, 90, 78],
"Jamal": ["678", 75, 88, 92],
"Zaid": ["912", 80, 79, 95]
}
sara_reg_num = students["Sara"][0]
print("Sara's Reg. No.: ", sara_reg_num)
jamal_marks = students["Jamal"][1]
print("Jamal's Marks for first subject are: ", jamal_marks)
students["Azra"] = ["234", 78, 85, 90]
print(students, "\n")
student_name = "Azra"
subject_index = 2
new_mark = 100
if student_name in students:
students[student_name][subject_index] = new_mark
print(f"*** Marks updated for {student_name} at index {subject_index} \n")
else:
print("Student not found \n")
for name, details in students.items():
print(f"Name: {name}")
print(f"Registration Number: ", {details[0]})
print(f"Marks: [", end='')
print(details[1], ',', details[2], ',', details[3], "]\n")Sara's Reg. No.: 345
Jamal's Marks for first subject are: 75
{'Sara': ['345', 85, 90, 78], 'Jamal': ['678', 75, 88, 92], 'Zaid': ['912', 80, 79, 95], 'Azra': ['234', 78, 85, 90]}
*** Marks updated for Azra at index 2
Name: Sara
Registration Number: {'345'}
Marks: [85 , 90 , 78 ]
Name: Jamal
Registration Number: {'678'}
Marks: [75 , 88 , 92 ]
Name: Zaid
Registration Number: {'912'}
Marks: [80 , 79 , 95 ]
Name: Azra
Registration Number: {'234'}
Marks: [78 , 100 , 90 ]- Lines 1 to 5: each value is a list; index 0 is the registration number and indexes 1 to 3 are marks.
- Line 7 reads Sara's registration number with two indexes: the key, then the list index .
- Line 9 selects Jamal's first-subject marks with .
- Line 12 inserts a new record for Azra and line 13 prints the whole dictionary.
- Lines 15 to 22 update Azra's second-subject marks: , but only if the key exists.
- Lines 24 to 28 loop over the key-value pairs and print every record.
# Each class studies 8 subjects and each teacher teaches 2 of them,
# so every class has 8 / 2 = 4 teachers stored in a list.
school = {
"Class 1": ["Ms. Amna", "Mr. Bilal", "Ms. Sadia", "Mr. Kamran"],
"Class 2": ["Ms. Hina", "Mr. Tariq", "Ms. Nida", "Mr. Usman"],
}
school["Class 3"] = ["Ms. Rabia", "Mr. Faisal", "Ms. Sana", "Mr. Imran"] # add a class
school["Class 2"][1] = "Mr. Waqas" # replace one teacher
for cls, teachers in school.items():
print(cls, "->", ", ".join(teachers), f"({len(teachers)} teachers)")Class 1 -> Ms. Amna, Mr. Bilal, Ms. Sadia, Mr. Kamran (4 teachers)
Class 2 -> Ms. Hina, Mr. Waqas, Ms. Nida, Mr. Usman (4 teachers)
Class 3 -> Ms. Rabia, Mr. Faisal, Ms. Sana, Mr. Imran (4 teachers)3.2.7Files in PythonOften examined
Importance of disk I/O
Disk input/output (disk I/O) means reading data from, and writing data to, a storage device such as a hard disk (HDD) or SSD. Disk I/O is the bridge between the CPU and the disk: relevant data is passed from the disk to the CPU for processing and is later stored back on permanent storage. Almost every computer operation depends on this continuous exchange: launching programs, opening files and saving the changes made by the user. Fast, efficient disk I/O is therefore vital for good system performance.
Variables live in RAM and are lost when a program ends. A file stores data permanently on the disk, so a program can save results and read them again later. Reading a file from the hard disk is an example of disk I/O; storing a value in a variable is not.
Types of files
| Text file | Binary file |
|---|---|
| Stores data as readable characters. | Stores data as a combination of 0s and 1s (binary data). |
| Each line ends with a special End Of Line (EOL) marker, usually , so Python knows a new line starts. | No line structure or EOL marker; data is a stream of bytes. |
| Can be opened and read by humans in any text editor. | Not meant for humans; read by other programs. |
| Examples: .txt, .csv | Examples: images (.jpg, .png), audio (.mp3), video (.mp4), programs (.exe) |
| Opened with modes "r", "w", "a" | Opened with modes "rb", "wb", "ab" |
File handling methods and operations
Data in a file is brought from the storage device into RAM, where the file can be created, read, updated and closed. Python's file handling methods are:
- open() gives access to a file. It takes 2 arguments: the file name (with the complete path if it is in another folder) and the mode of operation (reading, writing, appending and so on). It returns a file handler (file object) that is used for every later operation.
- read() extracts the contents of a file opened in read mode; the complete contents are returned as one string. reads one line and returns a list of lines.
- write() adds new material to a file opened in write or append mode.
- close() closes the file when work is finished. This guarantees data integrity (all data is really saved) and releases the system memory used by the file.
| Mode | Meaning | If the file does not exist | If the file exists |
|---|---|---|---|
| "r" | read (default) | FileNotFoundError | reads from the start |
| "w" | write | creates it | old contents are erased |
| "a" | append | creates it | new data is added at the end |
| "x" | create | creates it | FileExistsError |
| "r+" | read and write | FileNotFoundError | reads and writes |
| "rb", "wb", "ab" | the same modes for binary files | as above | as above |
new_file = open("abc.txt", "w")
new_file.write("The quick brown fox jumps over the lazy dog !!!")
print("File created and written to successfully!")
new_file.close()File created and written to successfully!The quick brown fox jumps over the lazy dog !!!- Line 1: creates abc.txt (if it does not already exist) and returns the file handler .
- Line 2: writes the sentence through the file handler.
- Line 4 tells the user that the file can now be opened and checked.
- Line 5: every opened file must be closed explicitly before the program ends; this is generally the last line.
with open("abc.txt", "w") as file:
file.write("The quick brown fox jumps over the lazy dog")
print("File created and written to successfully!")File created and written to successfully!The quick brown fox jumps over the lazy dogThe benefit of the keyword is that the file does not need to be closed explicitly: it is closed automatically when the indented block ends, even if an error occurs inside the block. Because mode "w" was used, the old sentence with '!!!' is overwritten.
with open("abc.txt", "r") as file:
for char in file.read():
print(char, end="")
print("\nFile read character by character successfully!")The quick brown fox jumps over the lazy dog
File read character by character successfully!Line 1 opens the file in read mode (an error occurs if the file is missing). Line 2: returns the whole text as a string and the for loop takes it one character at a time. Line 3 prints each character with so no new line is added after each character. Line 4 prints the confirmation on a new line ().
Handling file errors with try-except
Errors may arise while working with files, for example opening a file that does not exist, reading from a file, or problems when writing data. Such an error normally ends the program abnormally. To avoid this, file operations are placed in a try block and particular exceptions such as FileNotFoundError, PermissionError or IOError are handled in except blocks.
try:
with open("abc.txt", "r") as file:
for char in file.read():
print(char, end="")
print("\nFile read successfully")
except FileNotFoundError:
print("Error: The file does not exist.")
except IOError:
print("Error: An issue occurred while reading the file.")The quick brown fox jumps over the lazy dog
File read successfullyError: The file does not exist.If the file is missing, Python jumps from line 2 straight to the matching block, prints a friendly message and the program ends normally instead of crashing.
Appending and reading line by line
with open("notes.txt", "w") as f: # "w" creates the file (or empties it)
f.write("Line 1: Lists\n")
with open("notes.txt", "a") as f: # "a" adds at the end, old text is kept
f.write("Line 2: Files\n")
f.write("Line 3: Databases\n")
with open("notes.txt", "r") as f:
print(f.readline(), end="") # readline(): one line
rest = f.readlines() # readlines(): list of the remaining lines
print(rest)Line 1: Lists
['Line 2: Files\n', 'Line 3: Databases\n']A binary file is opened with a "b" in the mode, and Python reads and writes bytes instead of text:
data = bytes([72, 105, 33, 0, 255]) # five raw byte values 0 to 255
with open("data.bin", "wb") as f: # "wb": write binary
f.write(data)
with open("data.bin", "rb") as f: # "rb": read binary
content = f.read()
print(content)
print(list(content), len(content), "bytes")b'Hi!\x00\xff'
[72, 105, 33, 0, 255] 5 bytestry:
with open("abc.txt", "r") as source, open("xyz.txt", "w") as target:
count = 0
for char in source.read(): # read character by character
target.write(char) # write each character to xyz.txt
count += 1
print(count, "characters copied from abc.txt to xyz.txt")
with open("xyz.txt", "r") as check:
print("xyz.txt contains:", check.read())
except FileNotFoundError:
print("Error: abc.txt does not exist.")43 characters copied from abc.txt to xyz.txt
xyz.txt contains: The quick brown fox jumps over the lazy dog3.2.8GUI in Python
A graphical user interface (GUI) lets the user work with a program through windows, menus, text fields and buttons, instead of typing commands as in a command-line interface (CLI). A GUI improves usability and accessibility.
Tkinter is Python's built-in library for creating GUIs. It handles common tasks such as taking user input and deciding what happens when a button is clicked. With Tkinter programmers build desktop applications (word processors, media players, basic utilities) and simple 2D games. The arrangement of GUI components and the logic behind them are generally kept separate in the code.
- Widget
- A basic component of the interface: labels, buttons, text boxes (Entry), check boxes. Widgets are arranged and modified to get the required layout.
- Event handling
- Writing code that responds to events such as mouse clicks, key presses and window resizing. A Button's names the function to run when it is clicked.
- Main loop
- keeps the window open, waiting for and handling events. Without it the window would just flash and disappear.
| Tkinter item | Purpose |
|---|---|
| creates the main window that holds all components | |
| sets the window title | |
| shows text | |
| a one-line text box for input; reads it, clears it | |
| a button that calls function f when clicked | |
| / | place a widget in a row and column of a grid, or one below another |
| changes a widget's option (for example its text or colour) while the program runs |
Simple number adder with GUI
Steps to create an adder that takes two numbers and shows their sum when the Add button is pressed:
- Import the library: .
- Define : read both boxes with , convert them to numbers and show the sum in the result label; if the user types letters, a ValueError is caught and an error message is shown.
- Define : delete the text of both boxes from position 0 to the end and reset the label.
- Create the main window: and .
- Create two Entry boxes, 10 characters wide, in row 0 (columns 0 and 1) with 5 pixels of padding (, ).
- Add the buttons in row 1: "Add" calls and "Clear" calls .
- Add the result label in row 2, spread across both columns with .
- Run the GUI with .
import tkinter as tk
def add_numbers():
try:
num1 = float(entry1.get())
num2 = float(entry2.get())
result_label.config(text=f"Sum: {int(num1 + num2)}")
except ValueError:
result_label.config(text="Invalid input. Please enter numbers.")
def clear_fields():
entry1.delete(0, tk.END)
entry2.delete(0, tk.END)
result_label.config(text="Result will be displayed here.")
root = tk.Tk()
root.title("Simple Adder")
entry1 = tk.Entry(root, width=10)
entry1.grid(row=0, column=0, padx=5, pady=5)
entry2 = tk.Entry(root, width=10)
entry2.grid(row=0, column=1, padx=5, pady=5)
add_button = tk.Button(root, text="Add", command=add_numbers)
add_button.grid(row=1, column=0, padx=5, pady=5)
clear_button = tk.Button(root, text="Clear", command=clear_fields)
clear_button.grid(row=1, column=1, padx=5, pady=5)
result_label = tk.Label(root, text="Result will be displayed here.")
result_label.grid(row=2, column=0, columnspan=2, padx=5, pady=5)
root.mainloop()Developing a game using tkinter: the colour game
The word shown is the name of one colour, but it is displayed in a different (random) colour to confuse the player. The player must type the colour of the text, spelled correctly. Only a correct colour increases the score.
- Create a Python file and import and .
- Set a list of colours.
- Game logic: set . picks a random word and a random display colour , shows the word in that colour and clears the entry. compares the player's input with ; if correct the score increases, otherwise "(Incorrect)" is shown; then a new round starts.
- GUI: main window titled "Color Game", a label for the word, an entry for the answer, a Check button and a score label.
- Start: call for the first round, then .
import tkinter as tk
import random
window = tk.Tk()
window.title("Color Game")
score = 0
colors = ["red", "blue", "green", "yellow", "pink", "black",
"orange", "purple", "brown"]
def new_round():
global score
global color_to_guess, random_color
color_to_guess = random.choice(colors)
random_color = random.choice(colors)
label.config(text=color_to_guess, fg=random_color)
entry.delete(0, tk.END)
def check_answer():
global score
user_input = entry.get().lower()
if user_input == random_color:
score += 1
score_label.config(text="Score: " + str(score))
else:
score_label.config(text="Score: " + str(score) + " (Incorrect)")
new_round()
label = tk.Label(window, font=("Arial", 24))
label.pack(pady=20)
entry = tk.Entry(window, font=("Arial", 18))
entry.pack()
check_button = tk.Button(window, text="Check", command=check_answer)
check_button.pack(pady=10)
score_label = tk.Label(window, text="Score: 0")
score_label.pack()
new_round()
window.mainloop()Output: a window titled "Color Game" shows a colour name (for example "pink") written in another colour, an entry box, a Check button and the score line, such as "Score: 1 (Incorrect)" after a wrong answer. The keyword is needed because the functions change variables created outside them.
Pygame
Pygame is a Python library (installed with ) mainly used for 2D games. It opens a window, draws shapes and images, and reads keyboard and mouse events inside a game loop that runs many times per second. The steps for a Pygame GUI program are: import and initialise (), create the window (), run the game loop (handle events, update, draw), refresh the screen () and finally . A complete Pygame program is given in the answers to ERQ 7 and Mini project 1.
3.2.9DatabasesOften examined
A database is a set of data organised in a systematic manner, usually in a computer system. It stores, retrieves and handles a large number of records for multiple users, and lets users find specific information quickly and accurately. A database is the central source of data for almost every software program: when a user logs in, shops online or makes a transaction, the database saves the relevant data (login time, items purchased, account used) so it can be retrieved later.
Why databases are used
- They remove the redundancy (repetition) and inconsistency that are common in manual record keeping.
- They ensure the reliability of data.
- They allow concurrent access: several users can work on the same data at the same time.
- They provide security measures to protect sensitive information from unwanted access.
- They give effective information retrieval and help in data analysis.
Types of databases
| Relational database | NoSQL database |
|---|---|
| Organises data into tables of rows and columns; each table represents one entity (customers, items). | Does not have a fixed table structure. |
| Fixed schema; tables linked by keys. | Flexible; handles vast amounts of unstructured or semi-structured data. |
| Queried with SQL. Examples: MySQL, SQLite, PostgreSQL, MS Access. | Examples: MongoDB (documents), Redis (key-value). |
Key concepts
- Table
- The fundamental structure of a relational database, made of rows and columns. Each row is a record and each column is a field.
- Record
- A single row of a table holding information about one entity; for example a client record with name, address and telephone number.
- Field
- A piece of data within a record that defines the information placed in one column; for example a Name field stores text and an Age field stores a number.
- Data integrity
- The accuracy and consistency of data in a database, so that the data is correct, dependable and fit for use.
Primary and secondary keys
Keys uniquely identify records and create links among tables. The most common keys used in normalization are:
- Primary key
- A minimal set of attributes that uniquely identifies each record of a table. It can be one attribute or a combination of attributes. It cannot be empty (NULL) or repeated. Example: in the Customer table.
- Candidate key
- A single attribute or set of attributes that can identify a record uniquely. A table may have several candidate keys; one is chosen as the primary key and the rest are called secondary (alternate) keys. Example: in a Student table both RollNo and CNIC are candidate keys.
- Foreign key
- A field in one table that refers to the primary key of another table, creating a relationship between them. Example: in the Order table refers to the Customer table.
- Composite key
- A key made of two or more attributes that together identify a row uniquely. Example: (OrderID, L_ID) when one order can contain several laptops.
Data normalization
Data normalization is a database design technique that organises data into smaller, inter-linked tables to reduce redundancy (duplicate storage of the same data) and dependency. Redundant data causes problems in keeping data consistent and increases the chance of mistakes during updates. For example, if a customer's address is stored in many rows, every address change must be made in many places; missing one leaves the data inconsistent. Normalization stores the address once, in a Customer table, linked to other tables by a key such as the customer ID. Every table then represents one entity and keys define the relationships.
| Normal form | Rule |
|---|---|
| First normal form (1NF) | Every cell holds a single (atomic) value; repeating groups and multi-valued cells are removed. |
| Second normal form (2NF) | The table is in 1NF and every non-key attribute depends on the whole primary key (no partial dependency on part of a composite key). |
| Third normal form (3NF) | The table is in 2NF and no non-key attribute depends on another non-key attribute (no transitive dependency). |
Worked example: a laptop shop database
Step 1: identify the entities and their attributes. Customer: C_ID, C_Name, C_Address, C_Phone. Laptop: L_ID, L_Name, L_Price, L_Brand. Order: OrderID, OrderDate, C_ID, L_ID, Quantity.
Step 2: the unnormalized data in one table. Customer and laptop details repeat in every order:
| OrderID | OrderDate | C_ID | C_Name | C_Address | C_Phone | L_ID | L_Name | L_Price | L_Brand | Qty |
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 20-11-2024 | C1 | Qamar | House: 123 | 123-4567 | L1 | Dell XPS 13 | 100000 | Dell | 1 |
| 2 | 21-11-2024 | C2 | Mahtab | House: 456 | 987-6543 | L2 | MacBook Air | 90000 | Apple | 2 |
| 3 | 22-11-2024 | C1 | Qamar | House: 123 | 123-4567 | L3 | HP Spectre x360 | 120000 | HP | 1 |
| 4 | 23-11-2024 | C2 | Mahtab | House: 456 | 987-6543 | L1 | Dell XPS 13 | 100000 | Dell | 1 |
| 5 | 24-11-2024 | C3 | Chaand | House: 789 | 543-2109 | L2 | MacBook Air | 90000 | Apple | 2 |
Step 3: 1NF. The repeating groups are the customer information and the laptop information repeated for each order. They are moved to separate tables: Customer(C_ID, C_Name, C_Address, C_Phone), Laptop(L_ID, L_Name, L_Price, L_Brand) and Order(OrderID, OrderDate, C_ID, L_ID, Quantity).
| C_ID | C_Name | C_Address | C_Phone |
|---|---|---|---|
| C1 | Qamar | House: 123 | 123-4567 |
| C2 | Mahtab | House: 456 | 987-6543 |
| C3 | Chaand | House: 789 | 543-2109 |
| L_ID | L_Name | L_Price | L_Brand |
|---|---|---|---|
| L1 | Dell XPS 13 | 100000 | Dell |
| L2 | MacBook Air | 90000 | Apple |
| L3 | HP Spectre x360 | 120000 | HP |
Step 4: 2NF. An order may contain several laptops, so a row of the Order table is identified by the composite key (OrderID, L_ID). OrderDate depends only on OrderID, which is part of the key: a partial dependency. It is moved to a new table OrderDetails(OrderID, OrderDate), and the Order table keeps (OrderID, C_ID, L_ID, Quantity).
| OrderID | OrderDate |
|---|---|
| 1 | 20-11-2024 |
| 2 | 21-11-2024 |
| 3 | 22-11-2024 |
| 4 | 23-11-2024 |
| 5 | 24-11-2024 |
| OrderID | C_ID | L_ID | Quantity |
|---|---|---|---|
| 1 | C1 | L1 | 1 |
| 2 | C2 | L2 | 2 |
| 3 | C1 | L3 | 1 |
| 4 | C2 | L1 | 1 |
| 5 | C3 | L2 | 2 |
Step 5: 3NF. In the original table, L_Name, L_Price and L_Brand depend on L_ID, and L_ID depends on the order: a transitive dependency (OrderID L_ID L_Price). The same is true of the customer details through C_ID. In the tables above the Order table only references L_ID and C_ID (foreign keys) and every laptop or customer detail is stored once, so the four tables Customer, Laptop, Order and OrderDetails are in 3NF.
Database tools
| Microsoft Access | SQLite |
|---|---|
| Part of the MS Office suite; simple to install and easy to use with a drag-and-drop interface. | A lightweight database system that stores the whole database in a single file. |
| Used by individuals and small organisations for inventory, project management and event planning. | Ideal for applications needing local storage, widely used in mobile apps and where no large database server is needed. |
| Integrates with Excel and Project. | Simple, efficient and easy to use; built into Python as the sqlite3 module. |
Advantages of SQLite: it occupies little space, so it suits applications with limited resources; it is easily integrated into applications, so it is easy to use and deploy; it has a serverless architecture that removes unnecessary dependencies; and the complete database is saved in a single file.
3.2.10Create a database with SQLiteOften examined
1. Installation of SQLite
Download SQLite (DB Browser for SQLite) for your Windows version from the official website (https://sqlitebrowser.org/dl/). The download is a compressed (ZIP) file: right-click it and choose "Extract All" to get a folder of ready-to-use files. To check the installation, open the Command Prompt (type cmd in the Windows search bar), move to the extracted folder with the command (for example ), type and press Enter. If SQLite's own prompt appears, it is installed.
2. Database creation
Open the terminal, type and press Enter to enter the SQLite shell. The command creates a new database file NBF.db in the current folder (or opens it if it exists).
C:\Downloads\sqlite> sqlite3
sqlite> .open NBF.db3. Creating a table
takes the table name, the column names and their data types. The table Laptop has four columns: L_ID (text, primary key), L_Name (text), L_Price (integer) and L_Brand (text). means the column cannot be left empty. At this stage only the schema (structure) exists; the table has no records.
CREATE TABLE Laptop (
L_ID TEXT PRIMARY KEY,
L_Name TEXT NOT NULL,
L_Price INTEGER NOT NULL,
L_Brand TEXT NOT NULL
);4. Adding records
adds a row. It needs the table name, the column names and the values, in the same sequence as the columns.
INSERT INTO Laptop (L_ID, L_Name, L_Price, L_Brand)
VALUES ('L1', 'Dell XPS 13', 100000, 'Dell');Several records can be added with a single command:
INSERT INTO Laptop (L_ID, L_Name, L_Price, L_Brand)
VALUES
('L2', 'MacBook Air', 90000, 'Apple'),
('L3', 'HP Spectre x360', 120000, 'HP');L_ID L_Name L_Price L_Brand
---- --------------- ------- -------
L1 Dell XPS 13 100000 Dell
L2 MacBook Air 90000 Apple
L3 HP Spectre x360 120000 HPCREATE TABLE Customer (C_ID TEXT PRIMARY KEY, C_Name TEXT NOT NULL, C_Address TEXT, C_Phone TEXT);
CREATE TABLE OrderDetails (OrderID INTEGER PRIMARY KEY, OrderDate TEXT NOT NULL);
CREATE TABLE "Order" (OrderID INTEGER, C_ID TEXT, L_ID TEXT, Quantity INTEGER,
PRIMARY KEY (OrderID, L_ID),
FOREIGN KEY (OrderID) REFERENCES OrderDetails(OrderID),
FOREIGN KEY (C_ID) REFERENCES Customer(C_ID));
INSERT INTO Customer VALUES
('C1', 'Qamar', 'House: 123', '123-4567'),
('C2', 'Mahtab', 'House: 456', '987-6543'),
('C3', 'Chaand', 'House: 789', '543-2109');
INSERT INTO OrderDetails VALUES
(1, '20-11-2024'), (2, '21-11-2024'), (3, '22-11-2024'), (4, '23-11-2024'), (5, '24-11-2024');
INSERT INTO "Order" VALUES
(1, 'C1', 'L1', 1), (2, 'C2', 'L2', 2), (3, 'C1', 'L3', 1), (4, 'C2', 'L1', 1), (5, 'C3', 'L2', 2);To check that the tables are linked correctly, a query can join them through their keys:
SELECT o.OrderID, d.OrderDate, c.C_Name, o.L_ID, o.Quantity
FROM "Order" o
JOIN OrderDetails d ON o.OrderID = d.OrderID
JOIN Customer c ON o.C_ID = c.C_ID;OrderID OrderDate C_Name L_ID Quantity
------- ---------- ------ ---- --------
1 20-11-2024 Qamar L1 1
2 21-11-2024 Mahtab L2 2
3 22-11-2024 Qamar L3 1
4 23-11-2024 Mahtab L1 1
5 24-11-2024 Chaand L2 2OrderDetails has OrderID as its primary key. In Order the composite key is (OrderID, L_ID), and OrderID and C_ID are foreign keys that point to OrderDetails and Customer.
5. Querying records with SELECT
is the most used query. means all columns; alternatively list the column names to show only those columns. A clause adds a condition, so only the matching record(s) are fetched.
SELECT * FROM Customer;C_ID C_Name C_Address C_Phone
---- ------ ---------- --------
C1 Qamar House: 123 123-4567
C2 Mahtab House: 456 987-6543
C3 Chaand House: 789 543-2109SELECT * FROM Customer
WHERE C_ID = 'C2';C_ID C_Name C_Address C_Phone
---- ------ ---------- --------
C2 Mahtab House: 456 987-6543SELECT L_Name, L_Price FROM Laptop
WHERE L_Price >= 100000;L_Name L_Price
--------------- -------
Dell XPS 13 100000
HP Spectre x360 1200006. Update record
changes a record or a column. Good practice: first run a SELECT with the same WHERE clause to see which record will change, then run the UPDATE. Without WHERE, every row would be updated.
UPDATE Customer
SET C_Address = 'New Address'
WHERE C_ID = 'C1';C_ID C_Name C_Address C_Phone
---- ------ ----------- --------
C1 Qamar New Address 123-4567
C2 Mahtab House: 456 987-6543
C3 Chaand House: 789 543-21097. Use of ORDER BY clause
displays the result in sorted order, ascending by default. The keyword sorts in descending order.
SELECT * FROM Customer
ORDER BY C_Name;C_ID C_Name C_Address C_Phone
---- ------ ----------- --------
C3 Chaand House: 789 543-2109
C2 Mahtab House: 456 987-6543
C1 Qamar New Address 123-4567SELECT * FROM Customer
ORDER BY C_Name DESC;C_ID C_Name C_Address C_Phone
---- ------ ----------- --------
C1 Qamar New Address 123-4567
C2 Mahtab House: 456 987-6543
C3 Chaand House: 789 543-21098. Deletion of records
removes one or more records. The table structure stays in the database. Like SELECT, it works on all records if there is no WHERE clause, and only on the selected records with a WHERE clause. matches any value in a list. The name Order is written in quotes because ORDER is an SQL keyword.
DELETE FROM "Order" WHERE L_ID IN ('L1', 'L2', 'L3');OrderID C_ID L_ID Quantity
------- ---- ---- --------
(no rows)Connecting Python to a database
Python's built-in module lets a program use a database directly. The steps are: connect, create a cursor, execute SQL, commit the changes, fetch results and close the connection. The placeholders pass values safely into the SQL command.
import sqlite3
conn = sqlite3.connect("school.db") # 1. connect (creates the file if needed)
cur = conn.cursor() # 2. cursor: sends SQL to the database
cur.execute("""CREATE TABLE IF NOT EXISTS Student (
RollNo INTEGER PRIMARY KEY,
Name TEXT NOT NULL,
Marks INTEGER)""") # 3. run SQL commands
cur.execute("INSERT INTO Student VALUES (?, ?, ?)", (1, "Ali", 78))
cur.execute("INSERT INTO Student VALUES (?, ?, ?)", (2, "Sana", 91))
conn.commit() # 4. save the changes permanently
cur.execute("SELECT * FROM Student")
for row in cur.fetchall(): # 5. fetch the result rows
print(row)
conn.close() # 6. close the connection(1, 'Ali', 78)
(2, 'Sana', 91)3.3Techniques for testing and debuggingOften examined
Testing checks whether a program gives the correct results; it ensures the correctness of code. Debugging is the process of analysing the code to find the bugs (errors) that testing reveals, locating them and fixing them, so that the error-free program runs smoothly and gives the desired results.
| Error type | What happens | Example |
|---|---|---|
| Syntax error | The code breaks Python's grammar rules; the program does not start. | (missing bracket) |
| Runtime error (exception) | The program starts but stops when an illegal operation happens. | , KeyError, FileNotFoundError |
| Logical error | The program runs but gives a wrong answer. Hardest to find; debugging tools are needed. | swapped indexes in a transpose |
3.3.1DebuggingOften examined
Debugging with print statements
The simplest debugging method is to add print statements to check whether the program is behaving as expected. This does not mean a print after every line. Wherever a formula, an expression or a critical value update occurs, a print statement shows whether that variable, line or block is working as desired. Each print is labelled so we know which one produced the output.
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
transpose = []
for col in range(len(matrix[0])):
new_row = []
print('Print1-Outer Loop: ', new_row)
for row in range(len(matrix)):
new_row.append(matrix[col][row])
print('Print2-Inner Loop: row: ', row, '- col: ', col)
print('Print3-Outer Loop: ', new_row)
transpose.append(new_row)
for row in transpose:
print(row)Print1-Outer Loop: []
Print2-Inner Loop: row: 0 - col: 0
Print2-Inner Loop: row: 1 - col: 0
Print2-Inner Loop: row: 2 - col: 0
Print3-Outer Loop: [1, 2, 3]
Print1-Outer Loop: []
Print2-Inner Loop: row: 0 - col: 1
Print2-Inner Loop: row: 1 - col: 1
Print2-Inner Loop: row: 2 - col: 1
Print3-Outer Loop: [4, 5, 6]
Print1-Outer Loop: []
Print2-Inner Loop: row: 0 - col: 2
Print2-Inner Loop: row: 1 - col: 2
Print2-Inner Loop: row: 2 - col: 2
Print3-Outer Loop: [7, 8, 9]
[1, 2, 3]
[4, 5, 6]
[7, 8, 9]- 'Print1' (line 6) shows that is reset to [ ] each time the outer loop index changes, so this part is correct.
- 'Print2' (line 9) shows the index values inside the inner loop. While is 0, goes 0, 1, 2.
- 'Print3' (line 10) shows that the first new row is [1, 2, 3], which is the first row of the matrix, not the first column [1, 4, 7]. So line 8 reads (a row) when it should read (a column). The bug is found.
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
transpose = []
for col in range(len(matrix[0])):
new_row = []
for row in range(len(matrix)):
new_row.append(matrix[row][col]) # bug fixed: row first, then col
transpose.append(new_row)
for row in transpose:
print(row)[1, 4, 7]
[2, 5, 8]
[3, 6, 9]Advanced debugging: breakpoints and watchpoints
Advanced debugging uses tools of an IDE (such as Visual Studio, VS Code or the online IDE Replit) instead of print commands:
- Breakpoint
- A marker placed on a line of code. When the program is run in debug mode, it pauses just before that line, so the programmer can inspect the current values of variables and data structures.
- Watchpoint (watch)
- Set on a variable, expression or formula. The debugger shows its current value every time the program pauses, and can flag when it changes or differs from the expected value.
- Stepping
- Running the paused program one line at a time (step over, step into) to follow its flow.
Several breakpoints and watchpoints can be placed in one program; these methods are especially useful for complex problems. In Fig. 3.28 the transpose code is run in Replit's debugger with a breakpoint on line 7. At each pause the Variables panel shows col, row, new_row, matrix and transpose, which helps analyse the data and the behaviour of the program.
| Command (Python debugger, pdb) | Meaning |
|---|---|
| written in the code: pause here | |
| set a breakpoint on line 7 | |
| continue to the next breakpoint | |
| / | next line (step over) / step into a function |
| print the value of x | |
| watch x: show it whenever it changes | |
| quit the debugger |
3.3.2Python unit testOften examined
Unit testing checks small, isolated parts (units) of a program, such as a single function or method, to see that each works correctly. Instead of running the program by hand and checking values each time (which becomes difficult as the code grows), we write small tests once and run them again whenever the code changes. Python's built-in unittest framework supports this.
For a function that performs some operation, we write another function (a test function) that calls it with chosen inputs and uses assertions to compare the actual output with the expected output. The statement does nothing if the condition is True; if it is False it stops the program with an AssertionError that shows the message.
def add(a, b): # Function to be tested
return a + b
def test_add(): # Test function
result = add(2, 3) # Test case 1: Check if 2 + 3 equals 5
assert result == 5, f"Test failed: Expected 5 but got {result}"
result = add(0, 0) # Test case 2: Check if 0 + 0 equals 0
assert result == 0, f"Test failed: Expected 0 but got {result}"
result = add(-1, 1) # Test case 3: Check if -1 + 1 equals 0
assert result == 0, f"Test failed: Expected 0 but got {result}"
print("All tests passed!")
test_add() # Run the test functionAll tests passed!Line 1 defines . The test function on line 4 calls it with three sets of inputs (lines 5, 8 and 11). Each assert checks the result against the expected value. All three pass, so line 14 prints the message.
Now suppose the expected result on line 12 is wrongly written as 2 (the negative value on line 11 was overlooked). The test fails:
def add(a, b): # Function to be tested
return a + b
def test_add(): # Test function
result = add(2, 3) # Test case 1: Check if 2 + 3 equals 5
assert result == 5, f"Test failed: Expected 5 but got {result}"
result = add(0, 0) # Test case 2: Check if 0 + 0 equals 0
assert result == 0, f"Test failed: Expected 0 but got {result}"
result = add(-1, 1) # Test case 3: Check if -1 + 1 equals 0
assert result == 2, f"Test failed: Expected 2 but got {result}"
print("All tests passed!")
test_add() # Run the test functionTraceback (most recent call last):
File "prog.py", line 16, in <module>
test_add() # Run the test function
^^^^^^^^^^
File "prog.py", line 12, in test_add
assert result == 2, f"Test failed: Expected 2 but got {result}"
^^^^^^^^^^^
AssertionError: Test failed: Expected 2 but got 0To investigate, the book places breakpoints on lines 6, 9 and 12 and adds the variable as a watch (Fig. 3.30). At the first breakpoint the watch shows result = 5 (as expected, Fig. 3.31) and at the second result = 0 (as expected, Fig. 3.32). At the third breakpoint the watch shows result = 0 while line 12 expects 2, so the debugger flags the mismatch and points to the bug (Fig. 3.33).
def multiply(a, b):
return a * b
def test_multiply():
result = multiply(2, 3)
assert result == 6, f"Test failed: Expected 6 but got {result}"
result = multiply(4, 0)
assert result == 1, f"Test failed: Expected 1 but got {result}"
result = multiply(-1, -1)
assert result == 1, f"Test failed: Expected 1 but got {result}"
print("All tests passed!")
test_multiply()Traceback (most recent call last):
File "prog.py", line 16, in <module>
test_multiply()
File "prog.py", line 9, in test_multiply
assert result == 1, f"Test failed: Expected 1 but got {result}"
^^^^^^^^^^^
AssertionError: Test failed: Expected 1 but got 0Test case 1 passes (). Test case 2 fails: , but line 9 expects 1, so the assertion is False and the program stops with "Test failed: Expected 1 but got 0". Test case 3 is never reached (it would pass, since ). The bug is in the test, not in the function: line 9 should be . After this change all tests pass.
The unittest module
The unittest framework groups tests into a class derived from . Each method whose name starts with is one test case, and methods such as compare actual and expected results. runs all tests and reports which passed or failed.
import unittest
def add(a, b):
return a + b
class TestAdd(unittest.TestCase): # a group of test cases
def test_positive(self):
self.assertEqual(add(2, 3), 5)
def test_zero(self):
self.assertEqual(add(0, 0), 0)
def test_negative(self):
self.assertEqual(add(-1, 1), 0)
unittest.main(verbosity=2) # finds and runs every test_ methodtest_negative (__main__.TestAdd.test_negative) ... ok
test_positive (__main__.TestAdd.test_positive) ... ok
test_zero (__main__.TestAdd.test_zero) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OKKey terms
- Programming paradigm
- A style or methodology of writing and organising program code that decides its structure, flow and data management.
- Complexity reduction
- Breaking a complex problem into simpler parts (functions, objects, modules) that are easier to understand and solve.
- Functional programming
- A paradigm in which the program is built from functions; it focuses on what to compute and avoids changing state.
- Object oriented programming (OOP)
- A paradigm based on objects that combine data (attributes) and behaviour (methods), created from classes.
- Class
- A template or stencil that defines the attributes and methods of its objects.
- Object
- An instance of a class with its own attribute values, representing a real-world thing.
- Attribute
- A variable that stores a property (data) of an object.
- Method
- A function defined inside a class that describes the behaviour of its objects.
- Encapsulation
- Bundling data and the methods that use it into one class and restricting direct access to the data.
- Abstraction
- Showing only the essential features of an object and hiding the implementation details.
- Inheritance
- Creating a new (derived) class from an existing (parent) class so that it reuses the parent's attributes and methods.
- Polymorphism
- The ability of objects of different classes to respond to the same method call in their own way.
- List
- An ordered, mutable collection of items written in square brackets and accessed by index.
- Tuple
- An ordered, immutable collection written in round brackets; it cannot be changed after creation.
- Set
- An unordered collection of unique elements written in curly brackets; duplicates are removed automatically.
- Dictionary
- A collection of key-value pairs in which every key is unique and immutable and is used to look up its value.
- Nested list
- A list whose elements are lists; used to represent matrices and tables.
- Transpose
- A matrix obtained by swapping the rows and columns of another matrix.
- Disk I/O
- Reading data from and writing data to a storage disk; the bridge between the CPU and permanent storage.
- Text file
- A file that stores readable characters, with lines ended by an end-of-line marker.
- Binary file
- A file that stores raw bytes (0s and 1s), such as images, audio, video and programs.
- File handler
- The file object returned by open(), used for reading, writing and closing the file.
- File mode
- The second argument of open() that states the operation: r, w, a, x, r+, or their binary forms rb, wb, ab.
- Exception
- An error that occurs while a program runs, such as FileNotFoundError; handled with try-except.
- GUI
- Graphical user interface: interaction through windows, buttons, text fields and menus instead of typed commands.
- Tkinter
- Python's built-in library for creating GUI applications.
- Widget
- A basic GUI component such as a label, button, entry box or check box.
- Pygame
- A Python library used mainly for 2D games; it draws graphics and handles keyboard and mouse events.
- Database
- An organised collection of data that stores, retrieves and manages many records for multiple users.
- Relational database
- A database that stores data in tables of rows and columns linked by keys.
- NoSQL database
- A database without a fixed table structure, for large unstructured or semi-structured data.
- Table
- The basic structure of a relational database, made of rows (records) and columns (fields).
- Record
- One row of a table, holding information about one entity.
- Field
- One column of a table; a single piece of data in a record.
- Data integrity
- The accuracy and consistency of data in a database.
- Primary key
- A minimal set of attributes that uniquely identifies each record of a table; never repeated or empty.
- Candidate key
- An attribute or set of attributes that can uniquely identify a record; one is chosen as the primary key.
- Secondary key
- A candidate key that was not chosen as the primary key.
- Foreign key
- A field in one table that refers to the primary key of another table and links the two.
- Composite key
- A key made of two or more attributes that together identify a row uniquely.
- Data normalization
- Organising data into smaller related tables to reduce redundancy and dependency.
- Redundancy
- Unnecessary repetition of the same data in a database.
- 1NF
- First normal form: every cell holds a single atomic value; repeating groups are removed.
- 2NF
- Second normal form: 1NF plus every non-key attribute depends on the whole primary key.
- 3NF
- Third normal form: 2NF plus no non-key attribute depends on another non-key attribute (no transitive dependency).
- SQLite
- A lightweight, serverless database system that stores the whole database in a single file.
- SQL
- Structured Query Language, used to create tables and to insert, select, update and delete records.
- Testing
- Running a program or part of it to check that it gives correct results.
- Unit test
- A test of one small, isolated unit of code such as a single function or method.
- Assertion
- A statement (assert) that checks a condition and stops with an AssertionError if it is false.
- Debugging
- Finding, locating and fixing bugs (errors) in a program.
- Breakpoint
- A marker on a line of code where the debugger pauses the program.
- Watchpoint
- A variable or expression whose value the debugger shows and monitors while the program runs.
Multiple choice questionsp. 54–55
- AIncrease code length
- BComplexity reductionAnswer
- CEliminate bugs
- DImprove hardware performance
A paradigm organises code so that a complex problem is broken into simpler parts (functions or objects). It does not remove all bugs and has nothing to do with hardware.
- AProcedural
- BFunctionalAnswer
- CObject-oriented
- DStructural
Functional programming builds the program from functions that return values without changing variables outside themselves (no change of state). Procedural code changes state step by step.
- AData structures
- BFunctions
- CObjectsAnswer
- DVariables
OOP models the problem as objects (made from classes) that combine data (attributes) and behaviour (methods).
- AListAnswer
- BDictionary
- CTuple
- DSet
The list is Python's general-purpose ordered, changeable collection of items (Section 3.2.1). A dictionary stores key-value pairs, a tuple is fixed and a set holds only unique unordered items, so the expected answer is list.
- AKeys only
- BValues only
- CKey-value pairsAnswer
- DSequential data
Every entry of a dictionary links a unique key to a value, such as .
- AStoring a variable in memory
- BReading a file from the hard diskAnswer
- CDefining a function
- DLooping through a list
Disk I/O is data moving between the disk and the CPU/RAM. Variables, functions and loops work only in memory (RAM).
- Afile.write()Answer
- Bfile.insert()
- Cfile.add()
- Dfile.modify()
writes a string to a file opened in "w" or "a" mode. Python file objects have no insert(), add() or modify() methods.
- AA key that has a default value
- BA unique identifier for recordsAnswer
- CA key for linking multiple databases
- DA key used for temporary tables
A primary key uniquely identifies each record of a table; it cannot be repeated or empty. Linking tables is the job of a foreign key.
- AReducing redundancyAnswer
- BMaximizing data size
- CIncreasing the number of tables
- DStoring duplicate records
Normalization removes duplicate (redundant) data so that data stays consistent. More tables are a side effect, not the goal.
- APrimary
- BSecondary
- CForeignAnswer
- DUnique
A foreign key is a field in one table that refers to the primary key of a related table, so it identifies exactly one record in that related table (for example C_ID in the Order table points to one customer). A primary key identifies records in its own table.
- APygameAnswer
- BOS
- CSys
- Dmath
Of the options, only Pygame creates windows and graphics (the book's summary names Pygame). OS, sys and math have no GUI features. Tkinter, used in the book's examples, is also a GUI library but is not among the options.
- AA list containing another listAnswer
- BA list of dictionaries
- CA dictionary of lists
- DA set containing multiple lists
A nested list is a list whose elements are lists, such as . A set cannot even contain lists, because lists are mutable.
- ATest individual units of codeAnswer
- BTest the entire program
- CDebug the entire program
- DOptimize the code for speed
A unit test checks one small isolated unit, such as a single function or method.
- AWriting new code
- BFixing errors in the codeAnswer
- CIncreasing program speed
- DCreating a graphical interface
Debugging means locating and fixing bugs (errors) so that the program gives the desired results.
- Aprint()Answer
- Bdebug()
- Ctrace()
- Dlog()
The simplest debugging technique places labelled \texttt{print()} statements in the code. Python has no built-in debug() or trace() function.
my_dict = {"a": [1, 2], "b": [3, 4]}
print(my_dict["a"][1])- A1
- B2Answer
- C[1, 2]
- D[3, 4]
\texttt{my\_dict["a"]} is the list [1, 2] and index [1] is its second element, 2.
2for i in range(3):
for j in range(2):
print("Inner")- A2
- B3
- C5
- D6Answer
The outer loop runs 3 times and, for each of them, the inner loop runs 2 times, so the print runs 3\times2=6 times.
Short response questions (SRQs)p. 54
A programming paradigm is a style or methodology of writing and organising program code. It decides how the code is structured, how the program flows and how data is managed. Examples are the functional paradigm (program built from functions) and the object-oriented paradigm (program built from objects).
- It reduces complexity by breaking a big problem into smaller functions or objects.
- It gives a standard structure, so programs are easier to read, test, debug and maintain.
- It provides well-known tools and techniques, so teams can work together and focus on the task instead of inventing their own organisation.
Complexity reduction means breaking a large, complicated problem into smaller, simpler parts that are easier to understand and solve one at a time.
Dividing code into modules (functions, classes or separate files) helps because:
- each module does one job, so it can be written, understood and tested separately;
- a change or bug fix is made in one module only, without reading the whole program;
- modules can be reused in other programs, and different programmers can work on different modules at the same time.
For example, a result-card program can have separate functions for reading marks, calculating the percentage and printing the card.
| Functional programming | Object-oriented programming |
|---|---|
| The program is divided into functions. | The program is divided into objects made from classes. |
| Focus on what is to be computed; data flows through functions. | Focus on objects (data plus behaviour) that model real-world things. |
| Avoids changing state; functions have no side effects. | Objects keep state (attribute values) that methods change. |
| Data and functions are separate. | Data and methods are bundled together (encapsulation). |
| Code reused by combining functions. | Code reused through inheritance and polymorphism. |
| Suits small applications, data processing and parallel tasks. | Suits large, complex systems such as banking or games. |
Disk I/O (input/output) is the reading of data from, and writing of data to, a storage disk such as a hard disk or SSD. It is the bridge between the CPU and the disk: data is brought from the disk into RAM for the CPU to process, and results are stored back on the disk permanently.
It is important for performance because almost every operation depends on it: launching programs, opening files and saving the user's changes. The disk is much slower than RAM and the CPU, so slow or excessive disk I/O makes the whole system wait. Efficient disk I/O (for example reading a file once instead of many times) keeps programs fast.
The mode is the second argument of \texttt{open(filename, mode)} and tells Python what we want to do with the file:
- "r" (read): opens an existing file for reading; the default mode. Error if the file does not exist.
- "w" (write): creates a new file or erases an existing one, then writes to it.
- "a" (append): adds new data at the end of the file without erasing the old contents; creates the file if it does not exist.
- "x" (create): creates a new file; error if it already exists.
- "r+": read and write the same file.
- Adding b ("rb", "wb", "ab") opens a binary file such as an image.
f = open("marks.txt", "a")
f.write("Ali 85\n")
f.close()The \texttt{with} statement opens a file and closes it automatically when its indented block ends. We do not need to call \texttt{close()} ourselves.
Benefits: the file is closed even if an error occurs inside the block, so data is saved properly (data integrity) and the memory used by the file is released; the code is shorter and cleaner; and we cannot forget to close the file.
with open("abc.txt", "w") as file:
file.write("Hello")
# the file is already closed here- Install and import the library: \texttt{pip install pygame}, then \texttt{import pygame}.
- Initialise it: \texttt{pygame.init()}.
- Create the window: \texttt{screen = pygame.display.set\_mode((400, 300))} and set its title with \texttt{pygame.display.set\_caption()}.
- Write the main (game) loop: handle events with \texttt{pygame.event.get()} (QUIT, key presses, mouse clicks).
- Update and draw: fill the background, draw shapes or text (for example \texttt{pygame.draw.rect()}).
- Refresh the display with \texttt{pygame.display.flip()} in every pass of the loop.
- When the loop ends, close the window with \texttt{pygame.quit()}.
A complete example is the answer to ERQ 7.
A nested list is a list whose elements are themselves lists. It stores data in rows and columns, like a table, so it is used for matrices, game boards and marks of several students in several subjects.
To represent a matrix, each inner list is one row. The element in row r and column c is \texttt{m[r][c]} (indexes start at 0), and a nested loop visits every element.
m = [[1, 2, 3],
[4, 5, 6]]
print(m[1][2]) # row 1, column 2
for row in m:
print(row)6
[1, 2, 3]
[4, 5, 6]Data normalization is a database design technique that divides data into smaller, related tables linked by keys, following rules called normal forms (1NF, 2NF, 3NF).
It is used to:
- reduce redundancy, so the same data (such as a customer's address) is stored only once;
- avoid inconsistencies and mistakes during insertion, update and deletion, since a change is made in one place only;
- keep data accurate (data integrity) and make storage efficient and database access faster.
Example: storing customer details in a separate Customer table instead of repeating them in every order.
Debugging is the process of analysing a program to find the bugs (errors) in it, locating them and fixing them, so that the program runs smoothly and gives the desired results. Techniques include labelled print statements and debugger tools such as breakpoints and watchpoints.
It is important because even a small logical error can give wrong results (for example a wrong bill or wrong exam result), crash the program or create security problems. Debugging makes software correct and reliable, saves the cost of fixing errors after release, and helps programmers understand how their code actually behaves.
Sample_dict = {"a": 1, "b": 2}
print(sample_dict["c"])There are two problems:
- Python is case-sensitive: the dictionary is created as \texttt{Sample\_dict} (capital S) but line 2 uses \texttt{sample\_dict}, which does not exist, so a NameError occurs first.
- Even with the name corrected, the key "c" is not in the dictionary (only "a" and "b"), so reading it raises a KeyError.
Traceback (most recent call last):
File "prog.py", line 2, in <module>
print(sample_dict["c"])
^^^^^^^^^^^
NameError: name 'sample_dict' is not defined. Did you mean: 'Sample_dict'?Correction: use the same name and check the key first, or use get():
sample_dict = {"a": 1, "b": 2}
print(sample_dict.get("c", "Key not found"))Key not foundown_list = [1, 2, 3]:
own_list.append(4, 5)The code will not run, so the list never gets a final value:
- Line 1 ends with a colon, which is a SyntaxError; Python stops before running anything.
- Line 2: \texttt{append()} takes exactly one argument, so \texttt{append(4, 5)} is a TypeError.
- The question names my_list, but the code uses own_list.
File "prog.py", line 1
own_list = [1, 2, 3]:
^
SyntaxError: invalid syntaxIf the colon is removed and the two numbers are added with \texttt{extend([4, 5])} (or appended one at a time), the final value is [1, 2, 3, 4, 5]. With \texttt{append((4, 5))} the tuple is added as one item: [1, 2, 3, (4, 5)].
own_list = [1, 2, 3]
own_list.extend([4, 5])
print(own_list)[1, 2, 3, 4, 5]Extended response questions (ERQs)p. 52–53
Idea
The factorial of a whole number n is n!=1\times2\times3\times\dots\times n, with 0!=1. It is not defined for negative numbers. A loop multiplies the numbers from 2 to n; the same result can be written recursively as n!=n\times(n-1)!.
Program
def factorial(n):
"""Return n! for a whole number n (n >= 0)."""
if n < 0:
return "Not defined for negative numbers"
result = 1
for i in range(2, n + 1): # multiply 2 x 3 x ... x n
result = result * i
return result
def factorial_recursive(n): # the same using recursion
if n == 0:
return 1
return n * factorial_recursive(n - 1)
for number in [0, 1, 5, 7, 10]: # test with 5 different inputs
print(f"{number}! = {factorial(number)} | recursive: {factorial_recursive(number)}")
print("-3! =", factorial(-3))0! = 1 | recursive: 1
1! = 1 | recursive: 1
5! = 120 | recursive: 120
7! = 5040 | recursive: 5040
10! = 3628800 | recursive: 3628800
-3! = Not defined for negative numbersExplanation
- Lines 3 and 4 reject negative input.
- Lines 5 to 7: \texttt{result} starts at 1 and is multiplied by every number from 2 to n. For n=0 or 1 the loop does not run, so the answer is 1.
- Lines 10 to 13: the recursive version calls itself with n-1 until it reaches the base case n=0.
- Lines 15 and 16 test both functions with the 5 inputs 0, 1, 5, 7 and 10; both give the same answers, which checks each other. Line 17 tests the invalid input.
| i | result = result * i |
|---|---|
| 2 | 1 × 2 = 2 |
| 3 | 2 × 3 = 6 |
| 4 | 6 × 4 = 24 |
| 5 | 24 × 5 = 120 |
Program
class Rectangle:
def __init__(self, length, width): # constructor stores the two sides
self.length = length
self.width = width
def area(self):
return self.length * self.width
def perimeter(self):
return 2 * (self.length + self.width)
r1 = Rectangle(5, 3) # instances (objects) of the class
r2 = Rectangle(10, 4.5)
r3 = Rectangle(7, 7)
for name, r in [("r1", r1), ("r2", r2), ("r3", r3)]:
print(f"{name}: {r.length} x {r.width} area = {r.area()} perimeter = {r.perimeter()}")r1: 5 x 3 area = 15 perimeter = 16
r2: 10 x 4.5 area = 45.0 perimeter = 29.0
r3: 7 x 7 area = 49 perimeter = 28Explanation
- Line 1: \texttt{class Rectangle} is the template. Its attributes are \texttt{length} and \texttt{width}.
- Lines 2 to 4: the constructor \texttt{\_\_init\_\_} runs when an object is created and stores the two sides in \texttt{self}, the object itself.
- Lines 6 and 7: \texttt{area()} returns length × width. Lines 9 and 10: \texttt{perimeter()} returns 2(length + width).
- Lines 12 to 14 create three instances; each has its own length and width but shares the methods.
- Lines 16 and 17 call the methods for every object. For r1: area =5\times3=15 and perimeter =2(5+3)=16.
How functions simplify the problem
The problem is split into four small jobs, each done by one function: add all numbers, find repeated values, remove repeats, and find the average. Each function is short and easy to test, and \texttt{average()} reuses \texttt{add\_all()} instead of repeating the loop. This is complexity reduction through functions.
Program
def add_all(numbers): # adds every number
total = 0
for n in numbers:
total += n
return total
def mark_repeated(numbers): # finds values that appear more than once
repeated = []
for n in numbers:
if numbers.count(n) > 1 and n not in repeated:
repeated.append(n)
return repeated
def remove_repeats(numbers): # keeps only the first copy of each value
unique = []
for n in numbers:
if n not in unique:
unique.append(n)
return unique
def average(numbers): # reuses add_all()
return add_all(numbers) / len(numbers)
data = [4, 7, 4, 9, 7, 5, 10, 4]
print("List:", data)
print("Sum of all numbers:", add_all(data))
print("Repeated values:", mark_repeated(data))
print("Average with repeating values:", round(average(data), 2))
unique = remove_repeats(data)
print("List without repeats:", unique)
print("Average without repeating values:", round(average(unique), 2))List: [4, 7, 4, 9, 7, 5, 10, 4]
Sum of all numbers: 50
Repeated values: [4, 7]
Average with repeating values: 6.25
List without repeats: [4, 7, 9, 5, 10]
Average without repeating values: 7.0Working
- The list has 8 values with sum 4+7+4+9+7+5+10+4=50, so the average with repeats is 50/8=6.25.
- 4 and 7 appear more than once, so they are marked as repeated.
- Without repeats the list is [4, 7, 9, 5, 10]: sum 35, average 35/5=7.0.
Program
grades = {"Ali": "B", "Sana": "A", "Hamza": "C"}
def show():
for name, grade in grades.items():
print(f" {name}: {grade}")
while True:
choice = input("1-Add 2-Update 3-Remove 4-Show 5-Exit: ")
if choice == "1":
name = input("Name: ")
if name in grades:
print(name, "already exists")
else:
grades[name] = input("Grade: ") # new key-value pair
print(name, "added")
elif choice == "2":
name = input("Name: ")
if name in grades:
grades[name] = input("New grade: ") # existing key: value replaced
print(name, "updated")
else:
print(name, "not found")
elif choice == "3":
name = input("Name: ")
if name in grades:
del grades[name] # remove key and value
print(name, "removed")
else:
print(name, "not found")
elif choice == "4":
show()
elif choice == "5":
break1-Add 2-Update 3-Remove 4-Show 5-Exit: 4
Ali: B
Sana: A
Hamza: C
1-Add 2-Update 3-Remove 4-Show 5-Exit: 1
Name: Zara
Grade: A
Zara added
1-Add 2-Update 3-Remove 4-Show 5-Exit: 2
Name: Ali
New grade: A
Ali updated
1-Add 2-Update 3-Remove 4-Show 5-Exit: 3
Name: Hamza
Hamza removed
1-Add 2-Update 3-Remove 4-Show 5-Exit: 3
Name: Omar
Omar not found
1-Add 2-Update 3-Remove 4-Show 5-Exit: 4
Ali: A
Sana: A
Zara: A
1-Add 2-Update 3-Remove 4-Show 5-Exit: 5Explanation
- Line 1 creates the dictionary: names are keys, grades are values.
- The while loop shows a menu until the user enters 5 (\texttt{break}).
- Add: \texttt{grades[name] = grade} with a new key creates a new pair. The program first checks with \texttt{in} so an existing student is not overwritten by mistake.
- Update: the same assignment with an existing key replaces the old value (Ali's B becomes A).
- Remove: \texttt{del grades[name]} deletes the key and its value; the \texttt{in} check prevents a KeyError for a name that is not present (Omar).
- Show: \texttt{items()} gives every name-grade pair.
Program
# write mode creates data.txt (or empties it) and writes the string
with open("data.txt", "w") as f:
f.write("Python makes file handling easy.\nThis is the second line.")
print("data.txt created and written.")
# read mode opens the same file and reads the whole content
with open("data.txt", "r") as f:
content = f.read()
print("Content of data.txt:")
print(content)data.txt created and written.
Content of data.txt:
Python makes file handling easy.
This is the second line.Explanation
- Line 2: \texttt{open("data.txt", "w")} creates data.txt in the current folder (or empties it if it exists). \texttt{with} closes it automatically.
- Line 3: \texttt{write()} stores the string. \texttt{\textbackslash n} is the end-of-line marker, so the file has two lines.
- Line 7 opens the same file in read mode; line 8: \texttt{read()} returns the whole content as one string.
- Lines 9 and 10 display the content.
Idea
Append mode "a" does both jobs: it adds text at the end of an existing file without erasing it, and it creates the file if it does not exist. The program also reports which case happened using \texttt{os.path.exists()}.
Program
import os
def add_line(filename, text):
if os.path.exists(filename):
print(f"{filename} exists: appending a new line.")
else:
print(f"{filename} does not exist: creating it.")
with open(filename, "a") as f: # "a" appends, and creates the file if missing
f.write(text + "\n")
add_line("diary.txt", "Monday: revised lists") # first call: file is created
add_line("diary.txt", "Tuesday: revised files") # second call: line is appended
with open("diary.txt") as f:
print(f.read(), end="")diary.txt does not exist: creating it.
diary.txt exists: appending a new line.
Monday: revised lists
Tuesday: revised filesExplanation
- Line 4 checks whether the file is already on the disk.
- Line 8: mode "a" opens the file for appending (creating it the first time). Line 9 writes the text plus \texttt{\textbackslash n} so each entry is on its own line.
- The first call creates diary.txt; the second finds it and appends. Reading the file shows both lines, proving the old line was kept.
- If mode "w" were used instead, the second call would erase Monday's line.
Program
import pygame
pygame.init()
screen = pygame.display.set_mode((400, 300)) # the window
pygame.display.set_caption("Button Demo")
font = pygame.font.SysFont(None, 32)
button = pygame.Rect(140, 110, 120, 50) # x, y, width, height
message = ""
running = True
while running: # main loop
for event in pygame.event.get():
if event.type == pygame.QUIT: # window closed
running = False
elif event.type == pygame.MOUSEBUTTONDOWN and button.collidepoint(event.pos):
message = "Button clicked!" # click inside the button
screen.fill((255, 255, 255)) # white background
pygame.draw.rect(screen, (0, 120, 215), button) # blue button
screen.blit(font.render("Click me", True, (255, 255, 255)), (button.x + 14, button.y + 14))
if message:
screen.blit(font.render(message, True, (0, 0, 0)), (115, 200))
pygame.display.flip() # show the new frame
pygame.quit()Output
A 400 × 300 window titled "Button Demo" opens with a blue button labelled "Click me". When the user clicks inside the button, the text "Button clicked!" appears below it. Closing the window ends the program. (The program was tested by sending a mouse click at the button's centre: the message became "Button clicked!".)
Explanation
- Lines 3 to 6: initialise Pygame, create the window, set its title and choose a font.
- Line 7: \texttt{pygame.Rect(140, 110, 120, 50)} is the button's position and size.
- Lines 11 to 16: the main loop reads events. QUIT ends the loop; a MOUSEBUTTONDOWN event whose position lies inside the rectangle (\texttt{collidepoint}) sets the message.
- Lines 18 to 22 draw the background, the button, its label and the message; \texttt{render()} turns text into an image and \texttt{blit()} places it.
- Line 23: \texttt{flip()} shows the new frame. Line 25 closes Pygame.
Idea
The transpose swaps rows and columns: element (r, c) moves to (c, r). So column c of the matrix becomes row c of the transpose.
Program
def read_matrix():
return [[1, 2, 3],
[4, 5, 6],
[7, 8, 9]]
def transpose(m):
result = []
for col in range(len(m[0])): # each column of m ...
new_row = []
for row in range(len(m)):
new_row.append(m[row][col]) # ... becomes a row of the result
result.append(new_row)
return result
def show(m):
for row in m:
print(" ".join(str(x) for x in row))
A = read_matrix()
print("Matrix:")
show(A)
print("Transpose:")
show(transpose(A))Matrix:
1 2 3
4 5 6
7 8 9
Transpose:
1 4 7
2 5 8
3 6 9Explanation
- Lines 1 to 4: the matrix is a nested list; each inner list is a row.
- Lines 6 to 13: the outer loop takes each column index; the inner loop collects \texttt{m[row][col]} from every row, building one new row which is appended to the result.
- Lines 15 to 17 print each row with the numbers separated by spaces.
- Check: the first column of A is 1, 4, 7, which is the first row of the transpose.
| col | values collected (row = 0, 1, 2) | new row |
|---|---|---|
| 0 | m[0][0], m[1][0], m[2][0] | [1, 4, 7] |
| 1 | m[0][1], m[1][1], m[2][1] | [2, 5, 8] |
| 2 | m[0][2], m[1][2], m[2][2] | [3, 6, 9] |
Idea
Read the whole file, convert it to lower case so that 'P' and 'p' are counted together, and keep a dictionary in which each letter is a key and its count is the value.
Program
def count_letters(filename):
counts = {}
try:
with open(filename, "r") as f:
for ch in f.read().lower(): # count 'A' and 'a' together
if ch.isalpha(): # letters only
counts[ch] = counts.get(ch, 0) + 1
except FileNotFoundError:
print("File not found:", filename)
return counts
result = count_letters("story.txt")
for letter in sorted(result):
print(letter, ":", result[letter])a : 6
b : 1
c : 1
e : 1
f : 1
g : 1
h : 3
i : 6
k : 1
l : 1
m : 1
n : 4
o : 2
p : 1
r : 1
s : 4
t : 5
u : 4
y : 1Explanation
- Line 4 opens the file in read mode inside try, so a missing file gives a message instead of a crash.
- Line 5 reads the text and converts it to lower case; the loop takes one character at a time.
- Line 6: \texttt{isalpha()} skips spaces, full stops and new-line characters.
- Line 7: \texttt{counts.get(ch, 0)} returns the current count (0 the first time) and 1 is added.
- Lines 12 to 14 print the letters in alphabetical order using \texttt{sorted()}.
Program
import sqlite3
conn = sqlite3.connect("shop.db") # connect (creates shop.db if needed)
cur = conn.cursor()
cur.execute("DROP TABLE IF EXISTS Laptop")
cur.execute("""CREATE TABLE Laptop (
L_ID TEXT PRIMARY KEY,
L_Name TEXT NOT NULL,
L_Price INTEGER NOT NULL)""")
# INSERT: add records
laptops = [("L1", "Dell XPS 13", 100000), ("L2", "MacBook Air", 90000), ("L3", "HP Spectre x360", 120000)]
cur.executemany("INSERT INTO Laptop VALUES (?, ?, ?)", laptops)
conn.commit()
def show(title):
print(title)
cur.execute("SELECT * FROM Laptop ORDER BY L_ID") # RETRIEVE
for row in cur.fetchall():
print(" ", row)
show("After insert:")
# UPDATE: change the price of one laptop
cur.execute("UPDATE Laptop SET L_Price = ? WHERE L_ID = ?", (95000, "L2"))
conn.commit()
show("After update:")
cur.execute("SELECT L_Name FROM Laptop WHERE L_Price > ?", (95000,))
print("Laptops above 95000:", [r[0] for r in cur.fetchall()])
conn.close()After insert:
('L1', 'Dell XPS 13', 100000)
('L2', 'MacBook Air', 90000)
('L3', 'HP Spectre x360', 120000)
After update:
('L1', 'Dell XPS 13', 100000)
('L2', 'MacBook Air', 95000)
('L3', 'HP Spectre x360', 120000)
Laptops above 95000: ['Dell XPS 13', 'HP Spectre x360']Explanation
- Line 3: \texttt{sqlite3.connect()} connects to the database file (created if it does not exist). Line 4 makes a cursor that executes SQL.
- Line 5 removes any old copy of the table and lines 6 to 9 create it. Lines 12 and 13 insert three records; \texttt{?} placeholders pass the values safely. \texttt{commit()} saves them permanently.
- Line 18: \texttt{SELECT} with \texttt{fetchall()} retrieves the rows.
- Line 24 updates the price of L2 with \texttt{UPDATE ... SET ... WHERE}.
- Line 28 retrieves only the rows that satisfy a condition. Line 30 closes the connection.
Importance of database connectivity
- Permanent storage: data survives after the program ends, unlike lists and dictionaries in RAM.
- Large data: databases handle thousands of records efficiently and search them quickly with SQL.
- Sharing and concurrency: many users and applications can use the same data at the same time.
- Integrity and security: keys, constraints (such as NOT NULL) and access control keep data correct and safe.
- Real applications: login systems, online shops, school and bank systems all store their data through such connections.
Unit testing
Unit testing is testing the smallest parts (units) of a program, such as single functions or methods, in isolation. For each unit we write test cases that call it with chosen inputs and compare the actual result with the expected result using assertions. Python's built-in unittest framework groups test cases in a class derived from \texttt{unittest.TestCase} and reports which pass and which fail.
Importance and catching errors early
- Each function is checked as soon as it is written, so a bug is found while the code is small and fresh in the programmer's mind; it is much cheaper to fix than after the whole program is built.
- A failing test points to the exact function and input that is wrong, which narrows down debugging.
- Tests are saved and re-run automatically after every change (regression testing), so a new change that breaks old code is noticed immediately.
- Tests of boundary values (such as 40, 60, 80 marks) catch off-by-one mistakes that manual testing misses.
- Tests also document how a function is supposed to behave.
Example
import unittest
def calculate_grade(marks):
if marks < 0 or marks > 100:
return "Invalid"
if marks >= 80:
return "A"
if marks >= 60:
return "B"
if marks >= 40:
return "C"
return "F"
class TestGrade(unittest.TestCase):
def test_a_grade(self):
self.assertEqual(calculate_grade(85), "A")
def test_boundaries(self):
self.assertEqual(calculate_grade(80), "A")
self.assertEqual(calculate_grade(60), "B")
self.assertEqual(calculate_grade(40), "C")
def test_fail(self):
self.assertEqual(calculate_grade(39), "F")
def test_invalid(self):
self.assertEqual(calculate_grade(105), "Invalid")
unittest.main(verbosity=2)test_a_grade (__main__.TestGrade.test_a_grade) ... ok
test_boundaries (__main__.TestGrade.test_boundaries) ... ok
test_fail (__main__.TestGrade.test_fail) ... ok
test_invalid (__main__.TestGrade.test_invalid) ... ok
----------------------------------------------------------------------
Ran 4 tests in 0.000s
OKIf someone later changes \texttt{marks >= 80} to \texttt{marks > 80} by mistake, test_boundaries fails at once and reports the wrong value, before the error reaches users.
Program
import random
name = input("Hello, What's your name? ")
number = random.randint(1, 10) # secret number from 1 to 10
print(f"Okay {name}, I am guessing a number between 1 and 10:")
guessed = False
for attempt in range(1, 6): # at most 5 tries
guess = int(input(f"Try {attempt}: "))
if guess < number:
print("Your guess is too low")
elif guess > number:
print("Your guess is too high")
else:
guessed = True
break
if guessed:
print(f"Congratulations {name}! You guessed the number in {attempt} tries!")
else:
print(f"You did not guess the number. The number was {number}")Hello, What's your name? Abrar
Okay Abrar, I am guessing a number between 1 and 10:
Try 1: 5
Your guess is too low
Try 2: 8
Your guess is too high
Try 3: 7
Congratulations Abrar! You guessed the number in 3 tries!Hello, What's your name? Azam
Okay Azam, I am guessing a number between 1 and 10:
Try 1: 2
Your guess is too low
Try 2: 4
Your guess is too low
Try 3: 6
Your guess is too low
Try 4: 8
Your guess is too low
Try 5: 10
Your guess is too high
You did not guess the number. The number was 9Explanation
- Line 4: \texttt{random.randint(1, 10)} chooses the secret number; each run is different.
- Line 8: \texttt{for attempt in range(1, 6)} allows at most 5 tries and counts them.
- Lines 10 to 16 give the hint: too low, too high, or correct. On a correct guess \texttt{guessed} becomes True and \texttt{break} leaves the loop early.
- Lines 18 to 21: after the loop, the congratulation message shows the number of attempts; otherwise the secret number is revealed.
file_name = 'abc.txt'
target_letter = 'S'
try:
file = open(file_name, "r")
text = file.read()
file.close
except FileNotFoundError:
print(f"Error: File '{file_name}' not found.")
exit()
count = 0
for char in text:
if char = target_letter:
count += 1
print("The letter '{target_letter}' appears " + count + " times in the file '{file_name}'.")Bugs found
| Line | Bug | Correction |
|---|---|---|
| 2 | target_letter is 'S' (capital), but the text has lower-case s; the count would be 0. | \texttt{target\_letter = 's'} |
| 6 | \texttt{file.close} without brackets does not call the method, so the file is never closed. | \texttt{file.close()} |
| 12 | \texttt{if char = target\_letter:} uses the assignment operator; this is a SyntaxError, which is why the code does not execute. | \texttt{if char == target\_letter:} |
| 14 | A string and the integer count are joined with +, which is a TypeError; the {target_letter} and {file_name} placeholders are not in an f-string, so they would be printed literally. | use an f-string with {count} |
File "prog.py", line 12
if char = target_letter:
^^^^^^^^^^^^^^^^^^^^
SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='?Corrected program
file_name = 'abc.txt'
target_letter = 's' # 1. lower-case s, as in the sample output
try:
file = open(file_name, "r")
text = file.read()
file.close() # 2. brackets added: the method is now called
except FileNotFoundError:
print(f"Error: File '{file_name}' not found.")
exit()
count = 0
for char in text:
if char == target_letter: # 3. == compares, = assigns
count += 1
print(f"The letter '{target_letter}' appears {count} times in the file '{file_name}'.") # 4. f-stringThe letter 's' appears 3 times in the file 'abc.txt'.The word "scream" appears 3 times and each contains one 's'; no other word has an 's', so the count is 3, as in the sample output.
Mini projectsp. 52
Open-ended programming projects at the end of the unit. Pygame and tkinter programs open a window, so their result is described in words.
Plan
- A basket (rectangle) at the bottom moves left and right with the arrow keys.
- Three objects start at different heights above the window and fall at a fixed speed.
- If an object touches the basket, the score increases by 1 and that object restarts from the top at a random x position.
- If an object passes the bottom of the window, the game is over.
Program
import pygame
import random
pygame.init()
WIDTH, HEIGHT = 480, 600
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Catch The Falling Objects")
clock = pygame.time.Clock()
font = pygame.font.SysFont(None, 36)
basket = pygame.Rect(WIDTH // 2 - 50, HEIGHT - 30, 100, 20)
objects = [] # several falling objects
for i in range(3):
objects.append(pygame.Rect(random.randint(0, WIDTH - 20), -i * 200, 20, 20))
speed = 4
score = 0
game_over = False
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if not game_over:
keys = pygame.key.get_pressed() # arrow keys move the basket
if keys[pygame.K_LEFT] and basket.left > 0:
basket.x -= 8
if keys[pygame.K_RIGHT] and basket.right < WIDTH:
basket.x += 8
for obj in objects:
obj.y += speed
if obj.colliderect(basket): # caught: 1 point, restart at the top
score += 1
obj.x = random.randint(0, WIDTH - 20)
obj.y = -20
elif obj.top > HEIGHT: # missed: game ends
game_over = True
screen.fill((255, 255, 255))
pygame.draw.rect(screen, (0, 120, 215), basket)
for obj in objects:
pygame.draw.ellipse(screen, (220, 50, 50), obj)
screen.blit(font.render(f"Score: {score}", True, (0, 0, 0)), (10, 10))
if game_over:
screen.blit(font.render("Game Over", True, (0, 0, 0)), (WIDTH // 2 - 60, HEIGHT // 2))
pygame.display.flip()
clock.tick(60) # 60 frames per second
pygame.quit()Output and explanation
A 480 × 600 window shows red balls falling towards a blue basket, with "Score: n" at the top left. \texttt{colliderect()} detects a catch; \texttt{obj.top > HEIGHT} detects a miss and shows "Game Over". \texttt{clock.tick(60)} keeps the speed the same on every computer. The program was run with a test that let the objects fall without moving the basket: the game correctly ended with Game Over once an object was missed.
Plan
A \texttt{StringVar} is linked to the first Entry. \texttt{trace\_add("write", ...)} calls a function every time its text changes, that is, on every key press. The function copies the text in upper case into the second Entry, which is kept in the "readonly" state so the user cannot type in it.
Program
import tkinter as tk
def update_upper(*args):
output.config(state="normal") # unlock to change the text
output.delete(0, tk.END)
output.insert(0, text_var.get().upper()) # copy the text in upper case
output.config(state="readonly") # lock again: user cannot edit
root = tk.Tk()
root.title("Uppercase Converter")
tk.Label(root, text="Type here:").grid(row=0, column=0, padx=5, pady=5)
text_var = tk.StringVar()
text_var.trace_add("write", update_upper) # run update_upper on every key press
entry = tk.Entry(root, textvariable=text_var, width=30)
entry.grid(row=0, column=1, padx=5, pady=5)
tk.Label(root, text="Uppercase:").grid(row=1, column=0, padx=5, pady=5)
output = tk.Entry(root, width=30, state="readonly")
output.grid(row=1, column=1, padx=5, pady=5)
root.mainloop()Output and explanation
The window "Uppercase Converter" has two labelled boxes. Typing "hello world" in the first box immediately shows "HELLO WORLD" in the second. The second box must be switched to "normal" for a moment so the program itself can change it, then back to "readonly". The program was tested by typing text into the first box automatically; the second box showed the uppercase text and did not accept typing.
Important exam questions (not in the exercise)
Likely FBISE short and long questions from this unit that are not in the book's exercise, with full answers and tested programs.
| List | Tuple |
|---|---|
| Written in square brackets: \texttt{[1, 2]} | Written in round brackets: \texttt{(1, 2)} |
| Mutable: items can be added, changed or removed | Immutable: cannot be changed after it is created |
| Has methods append(), insert(), remove(), pop() | Has only count() and index() |
| Used for data that changes, such as a list of marks | Used for fixed data, such as days of the week or coordinates |
Example: \texttt{marks = [85, 92]} then \texttt{marks.append(78)} works; \texttt{days = ('Mon', 'Tue')} then \texttt{days[0] = 'Sun'} gives a TypeError.
A set is a collection of unique, unordered elements written in curly brackets. It has no index. Duplicate values are removed automatically: only one copy of each value is kept even if it is added many times. Sets are used for unique data such as usernames and for fast membership tests with \texttt{in}.
s = {5, 3, 5, 3, 8}
s.add(8)
print(s, len(s)){8, 3, 5} 3Both add an element to a list, but at different places:
- \texttt{append(x)} takes one argument and adds x at the end of the list.
- \texttt{insert(i, x)} takes two arguments and puts x at index i; the later elements move one place right. If i is larger than the length, x goes at the end.
a = [10, 20, 30]
a.append(40)
a.insert(1, 15)
print(a)[10, 15, 20, 30, 40]- \texttt{remove(x)} deletes the first element whose value is x. It returns nothing and raises a ValueError if x is not in the list.
- \texttt{pop(i)} deletes the element at index i and returns it, so the removed value can be used. Without an index it removes the last element. An invalid index raises an IndexError.
Use remove() when the value is known and pop() when the position is known.
g = [85, 92, 78, 95]
g.remove(78)
last = g.pop()
print(g, last)[85, 92] 95nums = [10, 20, 30, 40, 50]
print(nums[1:4])
print(nums[-2])
nums.insert(1, 15)
nums.pop()
print(nums, len(nums))[20, 30, 40]
40
[10, 15, 20, 30, 40] 5- \texttt{nums[1:4]} takes indexes 1, 2 and 3 (the slice stops before 4): [20, 30, 40].
- \texttt{nums[-2]} is the second element from the end: 40.
- \texttt{insert(1, 15)} gives [10, 15, 20, 30, 40, 50]; \texttt{pop()} removes the last element 50, leaving 5 elements.
Python stores each key at a position calculated from the key itself (its hash). If a key could change after being stored, its position would no longer match and the value could never be found again. So keys must be unchangeable types such as strings, numbers or tuples; a list cannot be a key.
For the same reason, searching a dictionary is fast: Python calculates the key's position and jumps straight to it, so the time hardly grows with the size. Searching a list checks the items one by one (linear search), so a longer list takes longer.
Encapsulation is information hiding: the data (attributes) and the methods that work on that data are bundled together in one unit, the class, and direct access to the data is restricted. The data can be read or changed only through the object's methods, which protects it from wrong or unwanted changes and hides the internal working.
Example: in a BankAccount class the balance is private (\texttt{self.\_\_balance}). It cannot be set directly; \texttt{deposit()} adds money, \texttt{withdraw()} subtracts it only if the balance is enough, and \texttt{get\_balance()} only reads it.
Polymorphism means "many forms". Objects of different classes are treated as objects of a common type, and the same method call behaves differently depending on the class of the object. It allows general code that works with many kinds of objects and makes behaviour easy to extend.
Example: Triangle, Rectangle and Circle are all Shapes and each has an \texttt{area()} method. In \texttt{for s in shapes: print(s.area())} the triangle uses \frac12bh, the rectangle l\times w and the circle \pi r^2, although the instruction is the same.
| Abstraction | Encapsulation |
|---|---|
| Shows only the essential features and hides the implementation details. | Bundles data with the methods that use it and restricts direct access to the data. |
| Deals with what an object does. | Deals with how the data is protected. |
| Achieved by designing simple method interfaces (start(), stop()). | Achieved with private attributes and getter/setter methods. |
| Example: a driver uses accelerate() without knowing how the engine burns fuel. | Example: a balance changed only through deposit() and withdraw(). |
- \texttt{read()}: reads the whole file (from the current position) and returns it as one string.
- \texttt{readline()}: reads one line, including its end-of-line character; \texttt{readlines()} returns all lines as a list.
- \texttt{write(s)}: writes the string s to a file opened in "w" or "a" mode and returns the number of characters written.
- \texttt{close()}: closes the file, making sure all data is saved and the memory is released.
f = open("abc.txt", "r")
first = f.readline()
rest = f.read()
f.close()- "w" (write): creates the file if it does not exist; if it exists, its old contents are erased first and the new data is written from the beginning.
- "a" (append): creates the file if it does not exist; if it exists, the old contents are kept and new data is added at the end.
So "w" is used to make a fresh file (for example a new report) and "a" to add records to an existing file (for example a log or attendance file). Opening an important file in "w" mode by mistake destroys its data.
File operations can fail for reasons outside the program: the file may not exist, the user may not have permission, or the disk may give a read or write error. Such an error (exception) would end the program abnormally. A try block contains the risky file code, and except blocks catch particular errors (FileNotFoundError, PermissionError, IOError) and handle them, for example by printing a clear message, so the program continues or ends normally.
try:
with open("marks.txt") as f:
print(f.read())
except FileNotFoundError:
print("marks.txt not found")marks.txt not found- Candidate key: any attribute or set of attributes that can uniquely identify a record. One candidate key is chosen as the primary key; the others are secondary keys. Example: RollNo and CNIC in a Student table.
- Foreign key: a field in one table that refers to the primary key of another table and links the two. Example: C_ID in the Order table refers to C_ID in the Customer table.
- Composite key: a key made of two or more attributes that together identify a row uniquely. Example: (OrderID, L_ID) in an order table where one order contains several laptops.
| Relational database | NoSQL database |
|---|---|
| Stores data in tables of rows and columns. | Stores data as documents, key-value pairs, graphs and so on. |
| Fixed structure (schema) that must be defined first. | Flexible structure; no fixed table layout. |
| Tables are linked by primary and foreign keys; queried with SQL. | Relations are less strict; each product has its own query method. |
| Best for structured data such as banking and school records. Examples: MySQL, SQLite. | Best for huge unstructured or semi-structured data such as social media posts. Examples: MongoDB, Redis. |
SELECT * FROM Customer ORDER BY C_Name DESC;UPDATE Customer SET C_Phone = '555-0000' WHERE C_ID = 'C3';DELETE FROM Customer WHERE C_ID = 'C2';In (b) and (c) the WHERE clause is essential: without it every customer's phone would be changed, or every record deleted. ORDER BY sorts in ascending order unless DESC is written.
| Breakpoint | Watchpoint (watch) |
|---|---|
| Placed on a line of code. | Set on a variable, expression or formula. |
| The program pauses just before that line in debug mode. | Its current value is shown at every pause, and a change or wrong value is flagged. |
| Answers: has the program reached this point, and in what state? | Answers: what is the value of this variable now? |
| Example: a breakpoint on line 12 of the test function. | Example: watching result, which showed 0 when 2 was expected. |
Both are tools of an IDE's debugger and are used together to locate logical errors without adding print statements.
Program
def read_marks(n):
marks = []
for i in range(1, n + 1):
marks.append(int(input(f"Marks of student {i}: ")))
return marks
def report(marks):
avg = sum(marks) / len(marks)
above = [m for m in marks if m > avg]
print("Highest:", max(marks), "| Lowest:", min(marks))
print("Average:", round(avg, 2))
print("Students above average:", len(above))
n = int(input("How many students? "))
report(read_marks(n))How many students? 5
Marks of student 1: 67
Marks of student 2: 82
Marks of student 3: 45
Marks of student 4: 90
Marks of student 5: 71
Highest: 90 | Lowest: 45
Average: 71.0
Students above average: 2Explanation
- \texttt{read\_marks(n)} repeats n times, converts each input to int and appends it to the list.
- \texttt{report()} uses the built-in functions \texttt{sum()}, \texttt{len()}, \texttt{max()} and \texttt{min()}.
- The list comprehension \texttt{[m for m in marks if m > avg]} keeps only the marks above the average.
- Check: total =67+82+45+90+71=355, average =355/5=71.0; 82 and 90 are above it, so the count is 2 (71 is not above 71).
Program
A = [[1, 2, 3],
[4, 5, 6],
[7, 8, 9]]
B = [[9, 8, 7],
[6, 5, 4],
[3, 2, 1]]
C = [] # result matrix
for r in range(3):
row = []
for c in range(3):
row.append(A[r][c] + B[r][c]) # add elements in the same position
C.append(row)
print("Sum of the matrices:")
for row in C:
for value in row:
print(value, end="\t")
print()
total = sum(A[i][i] for i in range(3)) # main diagonal: (0,0), (1,1), (2,2)
print("Sum of main diagonal of A:", total)Sum of the matrices:
10 10 10
10 10 10
10 10 10
Sum of main diagonal of A: 15Explanation
- Two matrices are added element by element: C[r][c]=A[r][c]+B[r][c]. The outer loop selects the row and the inner loop the column.
- Each completed row is appended to C, building a new nested list.
- The printing loop uses \texttt{end='\textbackslash t'} to keep a row on one line and \texttt{print()} to move to the next row.
- The main diagonal elements have equal row and column indexes, so 1+5+9=15.
| r | c | A[r][c] + B[r][c] |
|---|---|---|
| 0 | 0 | 1 + 9 = 10 |
| 0 | 1 | 2 + 8 = 10 |
| 1 | 2 | 6 + 4 = 10 |
| 2 | 2 | 9 + 1 = 10 |
Program
def copy_and_count(source, target):
lines = words = chars = 0
with open(source, "r") as src, open(target, "w") as dst:
for line in src: # a file object gives one line at a time
dst.write(line)
lines += 1
words += len(line.split())
chars += len(line)
print(f"Copied {source} to {target}")
print("Lines:", lines, "| Words:", words, "| Characters:", chars)
copy_and_count("poem.txt", "backup.txt")
with open("backup.txt") as f:
print(f.read(), end="")Copied poem.txt to backup.txt
Lines: 2 | Words: 10 | Characters: 54
Twinkle twinkle little star
How I wonder what you areExplanation
- The source is opened in read mode and the target in write mode in one \texttt{with} statement, so both close automatically.
- Looping over a file object gives one line at a time; each line is written unchanged to the target.
- \texttt{split()} breaks a line into words at spaces, so \texttt{len(line.split())} is the number of words.
- \texttt{len(line)} counts characters including the end-of-line character: 28+26=54.
The four principles
- Encapsulation: data and methods are bundled in a class and the data is changed only through methods. Here the balance is changed only by \texttt{deposit()} and \texttt{monthly\_update()}.
- Abstraction: the user sees only what is needed; \texttt{describe()} gives a simple summary and the calculation details stay hidden inside the class.
- Inheritance: SavingsAccount and CurrentAccount are derived from Account, so they get the constructor, \texttt{deposit()} and \texttt{describe()} without rewriting them.
- Polymorphism: both child classes have their own \texttt{monthly\_update()}; the same call adds profit to a savings account but deducts a charge from a current account.
Program
class Account: # parent class
def __init__(self, holder, balance=0):
self.holder = holder
self._balance = balance # protected: use the methods below
def deposit(self, amount): # encapsulation: controlled change
if amount > 0:
self._balance += amount
def get_balance(self):
return self._balance
def monthly_update(self): # common operation
pass
def describe(self): # abstraction: user sees a simple summary
return f"{self.holder}: Rs. {self._balance}"
class SavingsAccount(Account): # inheritance
def monthly_update(self): # polymorphism: own version
self._balance += self._balance * 0.01 # 1% profit
class CurrentAccount(Account):
def monthly_update(self):
self._balance -= 100 # service charge
accounts = [SavingsAccount("Hira", 50000), CurrentAccount("Usman", 20000)]
for acc in accounts:
acc.deposit(1000)
acc.monthly_update() # same call, different behaviour
print(type(acc).__name__, "->", acc.describe())SavingsAccount -> Hira: Rs. 51510.0
CurrentAccount -> Usman: Rs. 20900Working
Hira: 50000+1000=51000, plus 1% profit =51510. Usman: 20000+1000=21000, minus 100 =20900. The loop does not check the account type; each object applies its own rule.
Definition
Data normalization is the process of organising a database into smaller, related tables, following rules called normal forms, so that redundancy (repeated data) and dependency problems are removed. It prevents update, insert and delete anomalies and keeps data consistent.
Example: unnormalized table
| RollNo | Name | Class | ClassTeacher | Subjects |
|---|---|---|---|---|
| 1 | Ali | 12-A | Mr. Khan | Physics, CS |
| 2 | Sana | 12-A | Mr. Khan | Maths, CS |
First normal form (1NF)
Every cell must hold one (atomic) value. The Subjects cell holds two values, so each subject gets its own row. The key becomes (RollNo, Subject).
| RollNo | Subject | Name | Class | ClassTeacher |
|---|---|---|---|---|
| 1 | Physics | Ali | 12-A | Mr. Khan |
| 1 | CS | Ali | 12-A | Mr. Khan |
| 2 | Maths | Sana | 12-A | Mr. Khan |
| 2 | CS | Sana | 12-A | Mr. Khan |
Second normal form (2NF)
Every non-key attribute must depend on the whole key. Name and Class depend only on RollNo (part of the key), a partial dependency. They move to a Student table; the linking table keeps only (RollNo, Subject).
| RollNo | Name | Class | ClassTeacher |
|---|---|---|---|
| 1 | Ali | 12-A | Mr. Khan |
| 2 | Sana | 12-A | Mr. Khan |
| RollNo | Subject |
|---|---|
| 1 | Physics |
| 1 | CS |
| 2 | Maths |
| 2 | CS |
Third normal form (3NF)
No non-key attribute may depend on another non-key attribute. ClassTeacher depends on Class, not on RollNo (RollNo \to Class \to ClassTeacher is a transitive dependency). A Class table is created.
| RollNo | Name | Class |
|---|---|---|
| 1 | Ali | 12-A |
| 2 | Sana | 12-A |
| Class | ClassTeacher |
|---|---|
| 12-A | Mr. Khan |
Now if the class teacher changes, only one row is updated instead of every student's row.
Program
import sqlite3
conn = sqlite3.connect("college.db")
cur = conn.cursor()
cur.execute("DROP TABLE IF EXISTS Student")
cur.execute("CREATE TABLE Student (RollNo INTEGER PRIMARY KEY, Name TEXT NOT NULL, Marks INTEGER)")
cur.executemany("INSERT INTO Student VALUES (?, ?, ?)",
[(1, "Ali", 72), (2, "Sana", 91), (3, "Bilal", 85), (4, "Maryam", 64)])
conn.commit()
print("Students with marks above 80 (highest first):")
cur.execute("SELECT Name, Marks FROM Student WHERE Marks > 80 ORDER BY Marks DESC")
for name, marks in cur.fetchall():
print(" ", name, marks)
cur.execute("DELETE FROM Student WHERE RollNo = ?", (4,))
conn.commit()
cur.execute("SELECT COUNT(*) FROM Student")
print("Records left:", cur.fetchone()[0])
conn.close()Students with marks above 80 (highest first):
Sana 91
Bilal 85
Records left: 3Explanation
- \texttt{connect()} opens (or creates) college.db and the cursor executes SQL. The old table is dropped so the program can be run again.
- \texttt{CREATE TABLE} defines RollNo as the primary key and Name as NOT NULL.
- \texttt{executemany()} inserts four records in one call with ? placeholders; \texttt{commit()} saves them.
- \texttt{WHERE Marks > 80 ORDER BY Marks DESC} selects Sana (91) and Bilal (85), highest first.
- \texttt{DELETE ... WHERE RollNo = 4} removes Maryam's record; \texttt{COUNT(*)} confirms 3 records are left.
Program
def count_vowels(text):
return sum(1 for ch in text.lower() if ch in "aeiou")
def reverse(text):
return text[::-1] # slice with step -1 reads backwards
def is_palindrome(text):
cleaned = "".join(ch.lower() for ch in text if ch.isalnum())
return cleaned == reverse(cleaned)
for word in ["Level", "Python", "Madam, I'm Adam"]:
print(f"{word!r}: vowels = {count_vowels(word)}, reversed = {reverse(word)!r}, "
f"palindrome = {is_palindrome(word)}")'Level': vowels = 2, reversed = 'leveL', palindrome = True
'Python': vowels = 1, reversed = 'nohtyP', palindrome = False
"Madam, I'm Adam": vowels = 5, reversed = "madA m'I ,madaM", palindrome = TrueExplanation
- \texttt{count\_vowels()} converts the text to lower case and counts the characters that are in "aeiou".
- \texttt{reverse()} uses the slice \texttt{[::-1]}: start to end with step -1, which reads the string backwards.
- \texttt{is\_palindrome()} first keeps only letters and digits in lower case (\texttt{isalnum()}), so spaces, commas and capital letters do not matter, then compares the cleaned string with its reverse.
- "Madam, I'm Adam" becomes "madamimadam", which reads the same both ways, so it is a palindrome.
FBISE 2026 questions
Questions from the FBISE HSSC-II 2026 (1st Annual) papers that belong to this unit, answered the way an examiner expects.
- AEncapsulation
- BInheritanceAnswer
- CAbstraction
- DPolymorphism
Inheritance creates a derived (child) class from an existing parent class; the child reuses the parent's attributes and methods, so code is not repeated.
Data normalization (1 mark)
Data normalization is a database design technique that divides data into smaller, related tables linked by keys, so that redundancy (duplicate data) is reduced and inconsistencies during insertion, update and deletion are avoided.
Two forms (1 + 1 marks)
- First normal form (1NF): every cell holds a single (atomic) value; repeating groups and multi-valued cells are removed.
- Second normal form (2NF): the table is in 1NF and every non-key attribute depends on the whole primary key (no partial dependency).
- (Also: Third normal form (3NF): the table is in 2NF and no non-key attribute depends on another non-key attribute, that is, no transitive dependency.)
Opening a file (1.5 marks)
A file is opened with the built-in \texttt{open()} function, which takes the file name (with path if needed) and the mode ("r" read, "w" write, "a" append, "rb"/"wb" binary). It returns a file object (file handler) used for all later operations: \texttt{f = open("marks.txt", "w")}.
Closing a file (1.5 marks)
The file is closed with \texttt{f.close()}, which saves all data to the disk and frees the memory used by the file. Alternatively the \texttt{with} statement opens the file and closes it automatically when the block ends, even if an error occurs.
f = open("marks.txt", "w") # open: returns a file object
f.write("Ali 85\n")
f.close() # close: saves data, frees memory
with open("marks.txt", "r") as f: # opened here ...
print(f.read(), end="")
# ... and closed automatically hereAli 85Functional programming (1 mark)
Functional programming is a programming paradigm in which the program is divided into functions according to their functionality. The solution is broken into segments and each segment is handled by a function. The programmer focuses on what is to be computed rather than how, and functions return results without changing data outside themselves (no change of state, no side effects). Python supports this style with functions, \texttt{map()}, \texttt{filter()} and lambda.
def square(n):
return n * n
print(list(map(square, [1, 2, 3, 4])))[1, 4, 9, 16]Two pros (2 marks)
- Simpler, concise code that is easy to debug: each function does one job, so its logic is easy to follow and an error is traced to one function; a change affects only that function.
- Easy to test and suited to parallel work: a function's result depends only on its inputs, so it is tested on its own; because state is not shared and changed, functions can run in parallel (concurrently).
Two cons (2 marks)
- Not easy for beginners: thinking in functions without changing variables needs practice, and complex algorithms may need higher-order functions that are hard to express.
- Overhead and lower performance: creating new data instead of changing existing data costs extra memory and time, especially with large data structures, so it may be less efficient in performance-critical programs.
- ASegmentation of code into functions
- BEncapsulation, Abstraction, InheritanceAnswer
- CEmphasis on program flow only
- DWriting code without using objects
Encapsulation, abstraction and inheritance (with polymorphism) are the pillars of OOP. Dividing code into functions is the functional/procedural idea, and OOP is built on objects.
Concept (2 marks)
A primary key is a field, or a minimal set of fields, whose value uniquely identifies each record in a table. Its value can never be repeated and can never be empty (NULL), and each table has only one primary key. It is chosen from the candidate keys, and other tables link to the record through it as a foreign key. It prevents duplicate records and makes searching fast.
Example (1 mark)
In the Customer table, \texttt{C\_ID} is the primary key: C1, C2 and C3 each identify exactly one customer, although two customers may have the same name.
| C_ID (primary key) | C_Name | C_Phone |
|---|---|---|
| C1 | Qamar | 123-4567 |
| C2 | Mahtab | 987-6543 |
| C3 | Chaand | 543-2109 |
CREATE TABLE Customer (C_ID TEXT PRIMARY KEY, C_Name TEXT, C_Phone TEXT);- Not easy for beginners: it needs practice to solve problems only with functions and without changing variables.
- Overhead with large data: because data is not changed in place, new copies are created, which uses extra memory and time when handling large data structures; it may be less efficient in performance-critical tasks.
- Complex algorithms are harder to express: they often need higher-order functions (functions that take or return functions), and sharing data between functions needs careful handling and synchronisation.
| Text file | Binary file |
|---|---|
| Stores data as readable characters (ASCII/Unicode). | Stores data as raw bytes (0s and 1s), the same form as in memory. |
| Each line ends with an end-of-line marker (\texttt{\textbackslash n}); characters may be translated when read or written. | No end-of-line marker and no translation; bytes are stored exactly. |
| Can be read and edited by a person in any text editor. | Cannot be read by a person; needs the program that created it. |
| Usually larger for numbers (12345 needs 5 characters). | Usually smaller and faster to read and write (an int takes 4 bytes). |
| Examples: .txt, .csv, .py | Examples: .jpg, .png, .mp3, .mp4, .exe |
Any three rows earn the three marks. In Python a text file is opened with "r"/"w" and a binary file with "rb"/"wb".
Object-oriented programming (1 mark)
OOP is a programming paradigm based on objects. A class is a template that defines attributes (data) and methods (functions); an object is an instance of a class that models a real-world thing such as a car or a bank account. The emphasis in problem solving is on objects rather than logic, and OOP uses encapsulation, abstraction, inheritance and polymorphism.
class Car:
def __init__(self, color):
self.color = color
def start(self):
return self.color + " car started"
c = Car("Red")
print(c.start())Red car startedTwo pros (2 marks)
- Reusability and simpler complex systems: a big system is broken into smaller reusable classes, and inheritance lets new classes reuse existing code.
- Security and maintainability: encapsulation hides implementation details and protects data from direct access, so code is safer and easier to maintain; objects also represent real-world things naturally.
Two cons (2 marks)
- Complex design: if not planned carefully, class hierarchies become complex, and inheritance can make the design rigid and less flexible.
- Harder to understand and not always efficient: encapsulation and polymorphism can make code harder to follow and debug, and OOP may not suit computationally intensive tasks.
int a = 5, b = 10, c;
c = a++ * --b;
cout << c;- A45Answer
- B50
- C54
- D60
\texttt{a++} is post-increment: the old value 5 is used, then a becomes 6. \texttt{--b} is pre-decrement: b becomes 9 before it is used. So c=5\times9=45.
#include <iostream>
using namespace std;
int main() {
int a = 5, b = 10, c;
c = a++ * --b;
cout << c << endl;
cout << "a = " << a << ", b = " << b << endl;
return 0;
}45
a = 6, b = 9- A13
- B0
- C12Answer
- D11
Post-increment assigns the old value first (b = 12) and then increases a to 13.
#include <iostream>
using namespace std;
int main() {
int a = 12, b;
b = a++;
cout << "b = " << b << ", a = " << a << endl;
cout << "sizeof(float) = " << sizeof(float) << endl;
return 0;
}b = 12, a = 13
sizeof(float) = 4- A2
- B4Answer
- C6
- D8
A float is a single-precision real number stored in 4 bytes (32 bits); a double takes 8 bytes. The program in the previous question prints sizeof(float) = 4.
- AA[x++]
- BA[x]
- CA[7]
- DA[++x]Answer
Array indexes start at 0, so the seventh element is A[6]. \texttt{++x} increases x to 6 before it is used, giving A[6]. \texttt{A[x++]} and \texttt{A[x]} use A[5] (sixth element) and A[7] is the eighth element.
#include <iostream>
using namespace std;
int main() {
int A[10] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
int x = 5;
cout << A[++x] << endl; // x becomes 6 first: A[6] is the 7th element
return 0;
}70- AOne parameter
- BTwo parameters
- CThree parameters
- DAny number of parametersAnswer
A function may be defined with no parameters or with as many parameters as it needs; there is no fixed limit of one, two or three.
- A*Answer
- B&
- C**
- D&&
\texttt{*p} gives the value stored at the address held in pointer p. \texttt{\&} is the address-of (reference) operator and \texttt{\&\&} is logical AND.
- A&&
- B<=
- C%Answer
- D++
% (modulus) gives the remainder of a division and is one of the arithmetic operators + - * / %. && is logical, <= is relational and ++ is the increment (unary) operator.
- AProtected
- BPublic
- CPrivateAnswer
- DDefault
Members of a C++ class are private unless a specifier is written, which supports encapsulation. (In a struct the default is public.)
- ACommand
- BClassAnswer
- CObject
- DMethod
ofstream is a class of the fstream library for writing to files; we create an object of it, for example \texttt{ofstream fout("data.txt");}.
- Aget()
- Bfputs()
- Cfgets()
- Dput()Answer
\texttt{put()} writes a single character to an output stream (file). get() reads one character; fputs() and fgets() handle whole strings in C.
- \texttt{\textbackslash a} (alert): produces a beep (bell) sound from the computer.
- \texttt{\textbackslash b} (backspace): moves the cursor one position back, so the next character overwrites the previous one.
- \texttt{\textbackslash n} (new line): moves the cursor to the beginning of the next line.
#include <iostream>
using namespace std;
int main() {
cout << "Line 1\nLine 2" << endl; // \n: new line
cout << "ABC\bD" << endl; // \b: backspace, D overwrites C on screen
cout << "\a" << "Beep!" << endl; // \a: alert (bell sound)
return 0;
}Line 1
Line 2
ABC<BS>D
Beep!- 2a: a variable name cannot start with a digit; it must start with a letter or an underscore.
- S$: special characters such as $ are not allowed; only letters, digits and the underscore may be used.
- Int: it looks like the reserved keyword int (data type), and keywords cannot be used as variable names.
Purpose (1 mark)
The break statement immediately ends the loop (for, while, do-while) or switch statement in which it appears; control passes to the first statement after that loop or switch. It is used to leave a loop early when a condition is met, such as when a searched item is found.
Syntax and example (2 marks)
break;#include <iostream>
using namespace std;
int main() {
for (int i = 1; i <= 10; i++) {
if (i == 5)
break; // leave the loop when i is 5
cout << i << " ";
}
cout << endl << "Loop ended";
return 0;
}1 2 3 4
Loop ended| Text file | Binary file |
|---|---|
| Stores data as readable characters (ASCII/Unicode). | Stores data as raw bytes (0s and 1s), the same form as in memory. |
| Each line ends with an end-of-line marker (\texttt{\textbackslash n}); characters may be translated when read or written. | No end-of-line marker and no translation; bytes are stored exactly. |
| Can be read and edited by a person in any text editor. | Cannot be read by a person; needs the program that created it. |
| Usually larger for numbers (12345 needs 5 characters). | Usually smaller and faster to read and write (an int takes 4 bytes). |
| Examples: .txt, .csv, .py | Examples: .jpg, .png, .mp3, .mp4, .exe |
Any three rows give the three marks.
| Constructor | Destructor |
|---|---|
| Called automatically when an object is created. | Called automatically when an object is destroyed (goes out of scope). |
| Same name as the class: \texttt{Demo()}. | Same name with a tilde: \texttt{\textasciitilde Demo()}. |
| Initialises data members; can take parameters and can be overloaded. | Releases resources; takes no parameters and cannot be overloaded (one per class). |
#include <iostream>
using namespace std;
class Demo {
public:
Demo() { cout << "Constructor called" << endl; }
~Demo() { cout << "Destructor called" << endl; }
};
int main() {
{
Demo d; // object created: constructor runs
cout << "Inside block" << endl;
} // object goes out of scope: destructor runs
cout << "After block" << endl;
return 0;
}Constructor called
Inside block
Destructor called
After blockIn Python the constructor is \texttt{\_\_init\_\_()} and the destructor is \texttt{\_\_del\_\_()}.
cout<<p;
cout<<*p;- \texttt{cout<<p;} prints the value of the pointer itself, which is a memory address (for example 0x7ffd5c2a), the address of the variable it points to.
- \texttt{cout<<*p;} uses the dereference operator * and prints the value stored at that address, that is, the value of the variable p points to.
#include <iostream>
using namespace std;
int main() {
int n = 25;
int *p = &n; // p stores the address of n
cout << (p == &n) << endl; // cout << p would print this address, e.g. 0x7ffd...
cout << *p << endl; // value stored at that address
return 0;
}1
25Here the first line confirms that p holds the address of n (1 means true; printing p itself shows a different address on every run), and \texttt{*p} gives 25, the value of n.
A two-dimensional array stores values in rows and columns and is initialised with nested braces, one inner pair per row. Below, an int array holds marks of 2 students in 3 subjects and a float array holds temperatures.
#include <iostream>
using namespace std;
int main() {
int marks[2][3] = {{85, 90, 78}, {70, 88, 92}}; // integer 2D array
float temp[2][2] = {{30.5, 31.2}, {28.4, 29.9}}; // float 2D array
cout << "Marks:" << endl;
for (int r = 0; r < 2; r++) {
for (int c = 0; c < 3; c++)
cout << marks[r][c] << " ";
cout << endl;
}
cout << "Temperatures:" << endl;
for (int r = 0; r < 2; r++) {
for (int c = 0; c < 2; c++)
cout << temp[r][c] << " ";
cout << endl;
}
return 0;
}Marks:
85 90 78
70 88 92
Temperatures:
30.5 31.2
28.4 29.9 The nested for loops visit each row (outer loop) and each column (inner loop), just like a nested list in Python.
- I. x>0 is true (1) but y<10 is false (0); true && false is false, so A = 0.
- II. % has higher precedence than +, so 21\,\%\,4=1 and S = 13 + 1 = 14.
- III. \texttt{n *= 2} means n=n\times2=10\times2, so n = 20.
#include <iostream>
using namespace std;
int main() {
int x = 5, y = 15, n = 10;
int A = (x > 0) && (y < 10);
int S = 13 + 21 % 4;
n *= 2;
cout << "A = " << A << endl << "S = " << S << endl << "n = " << n << endl;
return 0;
}A = 0
S = 14
n = 20Purpose (1 mark)
The if-else statement selects one of two blocks of code. If the condition is true, the if block runs; otherwise the else block runs. It is used for two-way decisions such as pass/fail or even/odd.
Syntax with example (2 marks)
if (condition)
{
// statements when condition is true
}
else
{
// statements when condition is false
}#include <iostream>
using namespace std;
int main() {
int marks = 72;
if (marks >= 40)
cout << "Pass";
else
cout << "Fail";
return 0;
}PassA class is a template or blueprint that defines the attributes (data members) and methods (member functions) of a type of thing. An object is an instance of a class: a real entity created from that template, with its own values of the attributes. The class is only a design and uses no memory for data; memory is allocated when objects are created. One class can create many objects, all with the same structure and functions but different data, just as many houses can be built from one map.
#include <iostream>
using namespace std;
class Box { // class: the design
public:
int length;
};
int main() {
Box b1, b2; // objects: real boxes made from the design
b1.length = 10;
b2.length = 25;
cout << b1.length << " " << b2.length << endl;
return 0;
}10 25Importance
Comments are notes in the source code that the compiler ignores. They explain what the code does and why, so the program is easier to read, understand, debug and modify later, by the same programmer or by others. They can also switch off a line temporarily during debugging.
Two types
| Single-line comment | Multi-line comment |
|---|---|
| Starts with // and ends at the end of that line. | Starts with /* and ends with */; may cover several lines. |
| Used for short notes beside a statement. | Used for longer descriptions, such as the purpose of a program or function. |
| \texttt{x = x + 1; // increase x} | \texttt{/* This program finds the average of marks */} |
In Python a single-line comment starts with #.
Concept (2 marks)
Inheritance is an OOP feature in which a new class (derived or child class) is created from an existing class (base or parent class). The derived class automatically gets the data members and functions of the base class and can add its own. It gives code reusability, reduces repetition and builds a hierarchy of classes (an "is-a" relationship).
Daily life example (1 mark)
A car is a vehicle. The Vehicle class has wheels and start(); the Car class inherits them and adds its own member, seats. Similarly, a child inherits features from its parents.
#include <iostream>
using namespace std;
class Vehicle { // base class
public:
int wheels;
void start() { cout << "Vehicle started" << endl; }
};
class Car : public Vehicle { // derived class
public:
int seats;
};
int main() {
Car c;
c.wheels = 4; // inherited member
c.seats = 5;
c.start(); // inherited function
cout << c.wheels << " wheels, " << c.seats << " seats" << endl;
return 0;
}Vehicle started
4 wheels, 5 seatschar c = 'A';
do
{
cout << c << "\t";
c = c + 2;
}
while (c <= 'K');c starts at 'A'. Each pass prints c followed by a tab and adds 2 to its ASCII code, so the letters are A, C, E, G, I, K. After K, c becomes 'M', which is greater than 'K', so the loop stops.
#include <iostream>
using namespace std;
int main() {
char c = 'A';
do
{
cout << c << "\t";
c = c + 2;
} while (c <= 'K');
return 0;
}A C E G I K | Pass | c printed | c after c + 2 | c <= 'K'? |
|---|---|---|---|
| 1 | A (65) | C (67) | yes |
| 2 | C | E | yes |
| 3 | E | G | yes |
| 4 | G | I | yes |
| 5 | I | K (75) | yes |
| 6 | K | M (77) | no: stop |
int x = 5;
do {
cout << x << " ";
x++;
} while (x < 8);The do-while loop runs its body first and tests the condition afterwards. x = 5 is printed and becomes 6; 6 < 8, so 6 is printed and x becomes 7; 7 < 8, so 7 is printed and x becomes 8; now 8 < 8 is false and the loop stops. Output: 5 6 7
#include <iostream>
using namespace std;
int main() {
int x = 5;
do {
cout << x << " ";
x++;
} while (x < 8);
return 0;
}5 6 7 cout<<(((num%2)==0)?"Even\n":"Odd\n");The conditional (ternary) operator \texttt{condition ? a : b} gives a if the condition is true and b otherwise. The same decision with if-else:
if ((num % 2) == 0)
cout << "Even\n";
else
cout << "Odd\n";Tested with num = 7:
#include <iostream>
using namespace std;
int main() {
int num = 7;
if ((num % 2) == 0)
cout << "Even\n";
else
cout << "Odd\n";
return 0;
}Odd| Constant | Variable |
|---|---|
| A named value that cannot change while the program runs. | A named memory location whose value can change during execution. |
| Declared with const and must be given a value at declaration. | May be given a value at declaration or later, and changed any number of times. |
| \texttt{const float PI = 3.14159;} | \texttt{int radius = 2; radius = 3;} |
#include <iostream>
using namespace std;
int main() {
const float PI = 3.14159; // constant: cannot be changed
int radius = 2; // variable: can be changed
radius = 3;
cout << PI * radius * radius << endl;
return 0;
}28.2743Trying \texttt{PI = 3;} gives a compile-time error.
Purpose (2 marks)
\texttt{sizeof()} is an operator (used like a function) that returns the number of bytes a data type, variable or array occupies in memory. It helps write portable programs, because sizes may differ between computers, and it is used to find the number of elements of an array: \texttt{sizeof(arr) / sizeof(arr[0])}.
Example (1 mark)
#include <iostream>
using namespace std;
int main() {
int marks[10];
cout << sizeof(int) << " " << sizeof(char) << " " << sizeof(double) << endl;
cout << sizeof(marks) << endl; // 10 elements x 4 bytes
return 0;
}4 1 8
40The reference (address-of) operator & gives the memory address of a variable, as in \texttt{int *p = \&n;}. The same symbol is used in a declaration to create a reference variable, another name (alias) for an existing variable; a change through the reference changes the original. References are used to pass arguments by reference so a function can change them.
#include <iostream>
using namespace std;
void addBonus(int &m) { // m is a reference (another name) for the argument
m = m + 5;
}
int main() {
int marks = 70;
int &r = marks; // r refers to marks
r = 80; // changes marks
addBonus(marks);
cout << marks << endl;
return 0;
}85r = 80 changes marks to 80, and addBonus() adds 5 through its reference parameter, giving 85.
| Point | Local variable | Static (local) variable |
|---|---|---|
| Scope | Only inside the function or block where it is declared. | Also only inside its function or block. |
| Lifetime | Created when the function is called and destroyed when it returns; the value is lost. | Created once and exists until the program ends; keeps its value between calls. |
| Storage duration / initialisation | Automatic storage (stack); has garbage value unless initialised; initialised on every call. | Static storage; initialised only once (to 0 by default). |
#include <iostream>
using namespace std;
void counter() {
int local = 0; // created again on every call
static int total = 0; // created once, keeps its value between calls
local++;
total++;
cout << "local = " << local << ", static = " << total << endl;
}
int main() {
counter();
counter();
counter();
return 0;
}local = 1, static = 1
local = 1, static = 2
local = 1, static = 3| Actual parameters (arguments) | Formal parameters |
|---|---|
| Written in the function call. | Written in the function header (definition). |
| Can be constants, variables or expressions: sum(x, y), sum(4, 6). | Must be variables with data types: int a, int b. |
| Belong to the calling function. | Local to the called function; created when it is called and receive copies (or references) of the actual values. |
#include <iostream>
using namespace std;
int sum(int a, int b) { // a and b: formal parameters
return a + b;
}
int main() {
int x = 4, y = 6;
cout << sum(x, y) << endl; // x and y: actual parameters (arguments)
return 0;
}10int a (10), i;
cout>>"enter ten numbers";
for(i=0; i<10; i--)
cin << a(i);- \texttt{int a (10)} declares one integer initialised to 10, not an array: use square brackets \texttt{int a[10]}.
- cout uses the insertion operator <<, not >>.
- \texttt{i--} makes i go 0, -1, -2, ... so the loop never ends: use \texttt{i++}.
- cin uses the extraction operator >>, and array elements are written with square brackets: \texttt{cin >> a[i];}.
int a[10], i;
cout << "enter ten numbers";
for (i = 0; i < 10; i++)
cin >> a[i];#include <iostream>
using namespace std;
int main() {
int a[10], i;
cout << "enter ten numbers";
for (i = 0; i < 10; i++)
cin >> a[i];
cout << endl << "First: " << a[0] << ", last: " << a[9] << endl;
return 0;
}enter ten numbers
First: 1, last: 10| Array | String |
|---|---|
| A collection of elements of the same data type (int, float, char and so on) in consecutive memory. | A sequence of characters used to store text. |
| Has no end marker; its size is fixed when declared. | A C-string is a char array that ends with the null character \texttt{'\textbackslash 0'}; the string class manages its own length. |
| Processed element by element with a loop. | Has ready-made operations: length(), joining with +, comparison. |
| \texttt{int marks[3] = \{80, 75, 90\};} | \texttt{string name = "Ayesha";} |
#include <iostream>
#include <string>
using namespace std;
int main() {
int marks[3] = {80, 75, 90}; // array: many values of one type
string name = "Ayesha"; // string: sequence of characters
char city[] = "Lahore"; // C-string: char array ending in '\0'
cout << marks[1] << " " << name.length() << " " << sizeof(city) << endl;
return 0;
}75 6 7sizeof(city) is 7: six letters of Lahore plus the null character.
The class has public data members rollNo, name and marks, and the public member function get() that inputs them; show() displays them. Because the members are public, they can also be used directly from main().
#include <iostream>
#include <string>
using namespace std;
class Student {
public: // public data members
int rollNo;
string name;
float marks;
void get() { // member function to input the data
cin >> rollNo >> name >> marks;
}
void show() {
cout << rollNo << " " << name << " " << marks << endl;
}
};
int main() {
Student s; // object of class Student
s.get();
s.show();
return 0;
}12 Hina 88.5Function overloading means defining several functions with the same name but different parameter lists (number or types). It is a form of polymorphism.
- Readability: one meaningful name (area) is used for similar tasks instead of many names (areaSquare, areaRectangle).
- Flexibility: the same function can be called with different types or numbers of arguments; the compiler picks the right version automatically.
- Less effort and easier maintenance: the programmer remembers fewer names and related code stays together, which makes the program easier to extend.
#include <iostream>
using namespace std;
int area(int side) { return side * side; } // square
int area(int l, int w) { return l * w; } // rectangle
double area(double r) { return 3.14 * r * r; } // circle
int main() {
cout << area(4) << " " << area(5, 3) << " " << area(2.0) << endl;
return 0;
}16 15 12.56Exam focus
- What is functional programming? Two pros and two cons (and its drawbacks). (long)Asked in 2026 Set B Section C (5 marks) and Set D Q.2 OR; learn Table 3.1.Go to answer
- What is OOP? Two pros and two cons; four pillars with examples. (long)2026 Set D Section C (5 marks) and MCQs on inheritance and OOP concepts in both sets.Go to answer
- Data normalization and 1NF, 2NF, 3NF with an example. (short)2026 Set B Q.2 (vi); book SRQ 9; a natural long question.Go to answer
- Primary, candidate, foreign and composite keys. (short)2026 Set D Q.2 (vi) and book exercise MCQs 8 and 10.Go to answer
- Text versus binary files; opening and closing a file; file modes. (short)2026 Set D Q.2 (xi) OR, Set B Q.2 (ix) OR, old syllabus Q.2 (iii); book SRQs 5 and 6.Go to answer
- File programs: write then read data.txt, append a line, count letters, copy a file. (long)Book ERQs 5, 6, 9 and 13; the most likely write-a-program questions.Go to answer
- List, tuple, set and dictionary operations and outputs (insert, append, remove, pop, del, KeyError). (short)Book exercise MCQs 16 and 17 and SRQs 11 and 12 test predicting output.Go to answer
- Nested lists: matrix printing, transpose, symmetric matrix. (long)Book ERQ 8 and Activities 3 and 4; trace tables earn marks.Go to answer
- Python sqlite3 program: insert, update, retrieve; SQL commands. (long)Book ERQ 10 and the whole of Section 3.2.10.Go to answer
- Unit testing with assert and unittest; breakpoints and watchpoints. (long)Book ERQ 11, SRQ 10 and the Fig. 3.34 question on which test fails.Go to answer
- Class programs such as Rectangle with area and perimeter. (long)Book ERQ 2; old-syllabus paper also asked for a Student class.Go to answer