Lists: many values, one name
A list (called an array in exams and in other languages) stores many values under one name, in order. Each value sits in a numbered slot. The number is its index.
scores = [12, 45, 33, 50]
Why start at 0? The index means "how many steps from the start". The first item is 0 steps away. Why −1 for the last? Think of it as "len(scores) − 1". Python lets you leave out the len part.
| Code | What it does | scores becomes / result |
|---|---|---|
| scores[0] | Read the first item | 12 |
| scores[-1] | Read the last item | 50 |
| scores[1] = 47 | Replace the item at index 1 | [12, 47, 33, 50] |
| scores.append(28) | Add to the end | [12, 45, 33, 50, 28] |
| scores.insert(1, 99) | Put 99 at index 1; the rest shuffle right | [12, 99, 45, 33, 50] |
| scores.remove(33) | Delete the first value 33 | [12, 45, 50] |
| scores.pop() | Delete and give back the last item | [12, 45, 33] and gives 50 |
| len(scores) | How many items | 4 |
| scores[10] | Read a slot that isn't there | IndexError: the program crashes |
List playground
Press an operation. Watch the indexes move. Blue = index from the front, orange = from the back.
remove(33) removes by value. pop(2) removes by index. Mixing them up is a classic bug.
2D lists, and looping over lists
A list can hold other lists. A list of lists is a 2D list: a grid with rows and columns, like a seating plan or a spreadsheet.
seats = [["Ana", "Ben", "Cai"], ["Dev", "Eli", "Fay"], ["Gus", "Hana", "Iqra"]] print(seats[1][2])
Row first, then column. Like reading: go down to the line, then along it. len(seats) = 3 rows. len(seats[0]) = 3 columns.
Looping over a list
Loop over the items
for s in scores: print(s)
Simplest. s holds each value in turn: 12, then 45, then 33, then 50. Use it when you don't need positions.
Loop over the indexes
for i in range(len(scores)): print(i, scores[i])
range(4) gives 0, 1, 2, 3. Use it when you need the position, or want to change items.
# total and average of a list total = 0 for s in scores: total = total + s print("Mean:", total / len(scores)) # every seat in a 2D list: a loop inside a loop for row in range(len(seats)): for col in range(len(seats[row])): print(row, col, seats[row][col])
The micro:bit's 5 × 5 LED screen is a 2D grid. display.set_pixel(x, y, 9) lights column x, row y. Careful: the micro:bit puts x (column) first, the opposite of seats[row][col].
Strings: text you can take apart
A string is a sequence of characters, so it has indexes too, just like a list. A space counts as a character. So does punctuation.
| Code (word = "Computing") | Result | Why |
|---|---|---|
| len(word) | 9 | 9 characters |
| word[0] | "C" | The first character |
| word[-1] | "g" | The last character |
| word[0:3] | "Com" | A slice: from 0 up to, not including, 3 |
| word[3:] | "puting" | Blank end = to the end |
| word[:4] | "Comp" | Blank start = from the start |
| word.upper() | "COMPUTING" | A new string in capitals. .lower() does the opposite. |
| word.find("put") | 3 | Index where "put" starts. Not there? −1. |
| "put" in word | True | Is it anywhere inside? True or False. |
Slicing visualiser
s[a:b] gives b − a characters. Start at index a, stop before index b. Check: "Computing"[2:6] → 6 − 2 = 4 characters → "mput".
Strings can't be changed in place. word[0] = "K" gives an error. Build a new one instead: word = "K" + word[1:]. Also, word.upper() on its own does nothing useful: you must store it, word = word.upper().
AQA pseudo-code writes SUBSTRING(0, 2, word) and LEN(word); its SUBSTRING includes both ends (positions 0 to 2 = 3 characters). OCR's reference language uses word.substring(0, 3) and word.length, like Python. Read the exam's own guide.
Subroutines: procedures and functions
A subroutine is a named chunk of code you can run whenever you need it. Scratch calls these "My Blocks". Python uses def.
Procedure: does a job
def greet(name): print("Hello, " + name + "!") greet("Ana") greet("Leo")
Prints, but gives nothing back. (Python secretly returns None.)
Function: works something out
def area(w, h): result = w * h return result a = area(4, 3) print(a + 1)
Returns a value, so the call can be used like a number: stored, printed, added to.
defdefines the subroutine. Nothing runs yet. It's a recipe written down, not cooked.wandhare parameters: placeholder names in the def line.area(4, 3)calls it. 4 and 3 are arguments: the real values. They're copied into w and h, in order.returnends the subroutine at once and sends a value back to where it was called.
Parameter = the name. Argument = the value. Memory trick: Parameter is the Placeholder; Argument is the Actual value.
| Why use subroutines? | In plain words |
|---|---|
| Decomposition | Break a big problem into small named jobs: get_guess(), check_guess(), show_score(). |
| Reuse | Write it once, call it many times. No copy-and-paste. |
| Fix once | A bug in one place gets fixed in one place. |
| Test alone | Each subroutine can be tested on its own before joining the rest. |
| Teamwork | Different people write different subroutines at the same time. |
| Readable | A good name explains what a block does: is_valid_score(s). |
print(), len(), input() and int() are built-in subroutines. len and int are functions (they return a value). Programmers call random.randint(1, 6) a library function: someone else wrote it.
Scope: local and global variables
A variable made inside a subroutine is local. It only exists while the subroutine runs, and only inside it. A variable made in the main program is global: it can be read everywhere.
Step through a function call
Local is safer. Two subroutines can both use a variable called
totalwithout clashing. Each has its own.Local saves memory. The local box is wiped as soon as the subroutine returns.
Globals are risky. Any part of the program could change them, so bugs are hard to trace. Pass values in as arguments and
returnresults instead.
In Python, assigning to a name inside a function makes a new local, even if a global has the same name. bonus = bonus + 1 inside a function crashes (UnboundLocalError). To change the global you must write global bonus first. Most of the time: don't.
Validation, verification and authentication
Validation checks input is sensible before the program uses it. It's the bouncer at the door: it doesn't know who you are, only whether you follow the rules.
| Check | Asks | Example | Python idea |
|---|---|---|---|
| Presence | Anything entered? | Name can't be blank | name != "" |
| Type | Right kind of data? | Age is a whole number | age.isdigit() |
| Range | Between limits? | Score 0 to 50 | 0 <= s <= 50 |
| Length | Right number of characters? | Password ≥ 8 characters | len(pw) >= 8 |
| Format | Right pattern? | Date as DD/MM/YYYY | d[2] == "/" |
| Lookup | On the allowed list? | Year group is 7, 8 or 9 | y in ["7","8","9"] |
def get_score(): while True: # keep asking until it's valid text = input("Score (0-50): ") if text == "": print("Please type something.") # presence check elif not text.isdigit(): print("Whole numbers only.") # type check elif int(text) > 50: print("0 to 50, please.") # range check else: return int(text)
Validation checks data is reasonable, not that it's true. A valid birthday can still be the wrong one. 23 passes a 0–50 check even if the real score was 32.
Verification checks data was typed correctly. Two ways: double entry (type your new password twice; they must match) and proofreading (a person checks against the original).
Defensive design and authentication
Defensive design means planning for users who make mistakes, and for users who attack. It has four parts: anticipate misuse, validate input, authenticate users, and keep the code maintainable (comments, good names, indentation, subroutines).
Authentication checks who you are: a password, a PIN, a fingerprint, or two-factor authentication. Validation comes first: there's no point checking a password that's blank.
Why does a lock-out matter so much? Without one, a computer can guess a billion passwords a second (brute force). With "3 tries, then wait 10 seconds", it gets 0.3 guesses a second.
By hand, for a 6-letter lowercase password, 3 tries then a 10-second lock. Press Next line.
Guesses per second: g = a ÷ s = 3 ÷ 10 = 0.3.
Possible passwords: CL = 266 = 26 × 26 × 26 × 26 × 26 × 26 = 308,915,776.
Time to try them all: T = 308,915,776 ÷ 0.3 = 1,029,719,253 seconds.
Seconds in a year ≈ 60 × 60 × 24 × 365.25 = 31,557,600. So T ≈ 1,029,719,253 ÷ 31,557,600 ≈ 32.6 years.
Without the lock-out, at 1,000,000,000 guesses a second: 308,915,776 ÷ 1,000,000,000 ≈ 0.3 seconds. Three lines of code turned a third of a second into 32 years.
Getting past authentication without permission is a crime. UK: Computer Misuse Act 1990, section 1 (unauthorised access). Spain: Código Penal art. 197 bis (accessing a system without authorisation) and art. 264 (damaging data). Programmers who store passwords also have a duty to protect them (UK GDPR + Data Protection Act 2018; in Spain, EU GDPR + LOPDGDD 3/2018).
Testing: normal, boundary, erroneous
Testing means trying to break your own program on purpose, before someone else does. You choose test data carefully, so a few tests cover every kind of input.
| Test data | Meaning | Examples (0–50) | Expected |
|---|---|---|---|
| Normal | Sensible, typical data | 23, 38 | Accepted |
| Boundary | At the very edges: the last valid values and the first invalid ones | 0, 50 (and −1, 51) | 0 and 50 accepted; −1 and 51 rejected |
| Erroneous | Wrong type, nonsense, or far outside | "hello", 3.7, blank, 75 | Rejected with a helpful message |
Why test the edges? Most bugs live there. A programmer types > when they meant >=. Normal data like 23 works either way. Only 50 shows the bug.
AQA 8525 uses normal (typical), boundary (extreme) and erroneous. OCR J277 uses four: normal, boundary, invalid (right type, wrong value, like 75) and erroneous (wrong type, like "hello").
Test-data classifier
The program accepts a whole number from 0 to 50. What kind of test data is this?
A test plan
A test plan is written before testing. It lists what you'll test, the data, and what should happen. Then you run each test and fill in what did happen.
| No. | What is tested | Test data | Type | Expected result | Actual result | Pass? |
|---|---|---|---|---|---|---|
| 1 | A typical score is accepted | 23 | Normal | Accepted, stored | Accepted | ✓ |
| 2 | Top edge is accepted | 50 | Boundary | Accepted | "0 to 50, please." | ✗ |
| 3 | Just past the top is rejected | 51 | Boundary | "0 to 50, please." | "0 to 50, please." | ✓ |
| 4 | Bottom edge is accepted | 0 | Boundary | Accepted | Accepted | ✓ |
| 5 | Text is rejected | hello | Erroneous | "Whole numbers only." | "Whole numbers only." | ✓ |
| 6 | Blank is rejected | (blank) | Erroneous | "Please type something." | "Please type something." | ✓ |
Test 2 failed. The code said if int(text) >= 50 where it should say > 50. Only boundary data could catch that. Fix it, then re-run every test: a fix can break something else.
| When | Name | What it means |
|---|---|---|
| While writing | Iterative testing | Test each subroutine as you finish it. Fix, test again. |
| At the end | Final (terminal) testing | Test the whole program against the original requirements. |
Files: keeping data after the program ends
Variables live in RAM, so they vanish when the program ends or the power goes off. To keep data, write it to a file in storage.
| Mode | Means | If the file exists… | If it doesn't… |
|---|---|---|---|
| "r" | read | Read it from the start | Error: FileNotFoundError |
| "w" | write | Wipes it first, then writes | Creates it |
| "a" | append | Adds to the end | Creates it |
Write (append) a line
file = open("scores.txt", "a") file.write("Ana,45\n") file.close()
\n is the new line character. Without it every score lands on one long line.
Read every line
with open("scores.txt", "r") as file: for line in file: name, score = line.strip().split(",") print(name, "scored", int(score))
line.strip()removes the hidden\nat the end of each line..split(",")cuts the string at every comma and gives a list: "Ana,45" → ["Ana", "45"].Everything read from a file is a string. Cast with
int()before doing maths.with open(…) as file:closes the file for you at the end of the block, even if there's an error. Prefer it.
Python often holds written data in a buffer in RAM and only saves it to the drive on close(). Forget to close, and the last scores may never be saved. An open file can also be locked so other programs can't use it.
Errors, and code that doesn't crash
There are three kinds of error. Each is found at a different time, in a different way.
| Error | What it is | Example | Runs? | How you find it |
|---|---|---|---|---|
| Syntax error | Breaks the language's grammar | print("Hi" | No, not at all | The IDE (Thonny) tells you before it runs |
| Runtime error | Something impossible while running | 10 / 0, int("hi"), scores[10] | Starts, then crashes | An error message with a line number |
| Logic error | Runs fine, wrong answer | >= instead of > | Yes: the sneakiest | Only by testing, or a trace table |
try / except: catch a runtime error
while True: try: age = int(input("Age: ")) # might crash on "ten" break # it worked: leave the loop except ValueError: print("Please type digits, like 14.") print("Next year you'll be", age + 1)
Python tries the lines under
try:.The user types ten.
int("ten")can't work, so Python raises a ValueError.Instead of crashing, it jumps straight to
except ValueError:and prints the friendly message. Thebreakis skipped.The loop goes round. The user types 14.
int("14")works,breakleaves the loop.Output: Next year you'll be 15. The program never crashed: it is robust.
| Exception | Caused by |
|---|---|
| ValueError | Right type, wrong value: int("ten") |
| ZeroDivisionError | x / 0 |
| IndexError | A list index that doesn't exist |
| FileNotFoundError | Opening a missing file with "r" |
| NameError | Using a variable that doesn't exist (often a scope problem) |
A bare except: with no error name catches everything, even your own typos. Name the error you expect. And try/except can't catch a logic error: the program doesn't know it's wrong.
Common mix-ups
The mistakes nearly everyone makes the first time. Read the left side, then the right.
The first item of a list is scores[1].
It's scores[0]. The last item of a list of 4 is index 3, not 4.
word[0:3] gives 4 characters, 0 to 3.
It stops before 3: indexes 0, 1, 2. That's 3 − 0 = 3 characters.
Parameters and arguments are the same thing.
Parameter = the name in the def line. Argument = the value passed in the call.
print and return do the same job.
print shows a value to a person. return hands it back to the program to use.
A variable made inside a function can be used in the main program afterwards.
It's local: wiped when the function ends. Return it instead.
Valid data is correct data.
Validation only checks it's reasonable. Verification checks it was typed right. Neither proves it's true.
Boundary data means silly values.
Boundary = the edges (0, 50 and −1, 51). Silly values are erroneous.
Opening a file with "w" adds to it.
"w" wipes it first. Use "a" to add.
Check yourself
Quick questions first. Then endless practice: a wrong answer shows you the working, step by step.