Year 9 · Python II: Lists & Strings · Part C: Telling it what to do

Programming further

In one line

Real programs keep data in lists, take text apart as strings, split work into subroutines, check their input, save to files, and get tested on purpose.

1

Read and change lists (including 2D lists) using indexes that start at 0.

2

Slice strings with s[a:b] and predict the result, every time.

3

Write procedures and functions with parameters and return values, and explain local vs global scope.

4

Make programs robust: validation, authentication, try/except and file handling.

5

Plan tests with normal, boundary and erroneous data.

ListOne name holding many values in order.
IndexAn item's position, counting from 0.
SubroutineA named block of code you can call.
FunctionA subroutine that returns a value.
ParameterThe placeholder name in the def line.
ValidationChecking input is reasonable before using it.
Boundary dataTest data right at the edge of what's allowed.

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]
scores index from the front: 0, 1, 2, 3 → ← index from the back: −4, −3, −2, −1
Blue numbers count from the front, starting at 0. Orange numbers count from the back, starting at −1. Both point at the same boxes.
?Why?

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.

CodeWhat it doesscores becomes / result
scores[0]Read the first item12
scores[-1]Read the last item50
scores[1] = 47Replace 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 items4
scores[10]Read a slot that isn't thereIndexError: the program crashes
▶

List playground

Press an operation. Watch the indexes move. Blue = index from the front, orange = from the back.

scores =

!Watch out

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])
Fay
First bracket = row (which inner list). Second bracket = column (which item in it). seats[1][2] = row 1, column 2 = Fay.
★Remember

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])
e.g.micro:bit

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.

Think of the numbers as fence posts between letters. word[0:3] = everything between post 0 and post 3 = "Com". Letter 3 ("p") sits after post 3, so it is left out.
Code (word = "Computing")ResultWhy
len(word)99 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")3Index where "put" starts. Not there? −1.
"put" in wordTrueIs it anywhere inside? True or False.
▶

Slicing visualiser

start a0 stop b3
→
★Remember

s[a:b] gives b − a characters. Start at index a, stop before index b. Check: "Computing"[2:6] → 6 − 2 = 4 characters → "mput".

!Watch out

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

e.g.Exams

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.

Main program a = area(4, 3) 4 and 3 are the arguments a now holds 12 def area(w, h): w and h are the parameters w = 4, h = 3 result = w * h → 12 return result 1. call: copy 4 → w, 3 → h 2. return 12
Blue = the arguments going in (real values). Purple = the parameters that catch them (placeholder names). Green = the return value coming back.

Procedure: does a job

def greet(name):
    print("Hello, " + name + "!")

greet("Ana")
greet("Leo")
Hello, Ana! Hello, 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)
13

Returns a value, so the call can be used like a number: stored, printed, added to.

  1. def defines the subroutine. Nothing runs yet. It's a recipe written down, not cooked.

  2. w and h are parameters: placeholder names in the def line.

  3. area(4, 3) calls it. 4 and 3 are arguments: the real values. They're copied into w and h, in order.

  4. return ends the subroutine at once and sends a value back to where it was called.

★Remember

Parameter = the name. Argument = the value. Memory trick: Parameter is the Placeholder; Argument is the Actual value.

Why use subroutines?In plain words
DecompositionBreak a big problem into small named jobs: get_guess(), check_guess(), show_score().
ReuseWrite it once, call it many times. No copy-and-paste.
Fix onceA bug in one place gets fixed in one place.
Test aloneEach subroutine can be tested on its own before joining the rest.
TeamworkDifferent people write different subroutines at the same time.
ReadableA good name explains what a block does: is_valid_score(s).
e.g.You already use them

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.

GLOBAL (main program) bonus5 result15 LOCAL to add_bonus() score10 total15 Wiped when the subroutine returns. local can read global ✓ main can't see total ✗
Boxes inside boxes. Code inside the purple box can look out at the blue box. Code in the blue box can't look in.
▶

Step through a function call


    
Global
Local to add_bonus
Screen:

  1. Local is safer. Two subroutines can both use a variable called total without clashing. Each has its own.

  2. Local saves memory. The local box is wiped as soon as the subroutine returns.

  3. Globals are risky. Any part of the program could change them, so bugs are hard to trace. Pass values in as arguments and return results instead.

!Watch out

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.

CheckAsksExamplePython idea
PresenceAnything entered?Name can't be blankname != ""
TypeRight kind of data?Age is a whole numberage.isdigit()
RangeBetween limits?Score 0 to 500 <= s <= 50
LengthRight number of characters?Password ≥ 8 characterslen(pw) >= 8
FormatRight pattern?Date as DD/MM/YYYYd[2] == "/"
LookupOn the allowed list?Year group is 7, 8 or 9y 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 is not truth

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.

e.g.Verification

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.

Input ValidationIs it sensible? AuthenticationIs it correct? Access ✓ fails → message, ask again Wrong → tries + 1. At 3 tries: lock for10 seconds (a lock-out)
Yellow = validation (is it sensible?). Blue = authentication (is it the right one?). Red = the lock-out after too many wrong tries.

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.

  1. Guesses per second: g = a ÷ s = 3 ÷ 10 = 0.3.

  2. Possible passwords: CL = 266 = 26 × 26 × 26 × 26 × 26 × 26 = 308,915,776.

  3. Time to try them all: T = 308,915,776 ÷ 0.3 = 1,029,719,253 seconds.

  4. Seconds in a year ≈ 60 × 60 × 24 × 365.25 = 31,557,600. So T ≈ 1,029,719,253 ÷ 31,557,600 ≈ 32.6 years.

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

★The law

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.

Rule: accept a whole number from 0 to 50. Green = normal. Amber = boundary (right on the edge, and just past it). Red = erroneous.
Test dataMeaningExamples (0–50)Expected
NormalSensible, typical data23, 38Accepted
BoundaryAt the very edges: the last valid values and the first invalid ones0, 50 (and −1, 51)0 and 50 accepted; −1 and 51 rejected
ErroneousWrong type, nonsense, or far outside"hello", 3.7, blank, 75Rejected with a helpful message
?Why?

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.

e.g.Exam boards

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?

0 of 0 right

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 testedTest dataTypeExpected resultActual resultPass?
1A typical score is accepted23NormalAccepted, storedAccepted✓
2Top edge is accepted50BoundaryAccepted"0 to 50, please."✗
3Just past the top is rejected51Boundary"0 to 50, please.""0 to 50, please."✓
4Bottom edge is accepted0BoundaryAcceptedAccepted✓
5Text is rejectedhelloErroneous"Whole numbers only.""Whole numbers only."✓
6Blank is rejected(blank)Erroneous"Please type something.""Please type something."✓
!Spot the bug

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.

WhenNameWhat it means
While writingIterative testingTest each subroutine as you finish it. Fix, test again.
At the endFinal (terminal) testingTest 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.

RAM (volatile) name = "Ana"score = 45 Gone when the program ends. Storage: scores.txt Leo,38Ana,45 Still there tomorrow. 1. open 2. write 1. open 2. read 3. always close
Writing copies data out of RAM into the file. Reading copies it back in. Every time: open, use, close.
ModeMeansIf the file exists…If it doesn't…
"r"readRead it from the startError: FileNotFoundError
"w"writeWipes it first, then writesCreates it
"a"appendAdds to the endCreates 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))
Leo scored 38 Ana scored 45
  1. line.strip() removes the hidden \n at the end of each line.

  2. .split(",") cuts the string at every comma and gives a list: "Ana,45" → ["Ana", "45"].

  3. Everything read from a file is a string. Cast with int() before doing maths.

  4. with open(…) as file: closes the file for you at the end of the block, even if there's an error. Prefer it.

?Why close?

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.

ErrorWhat it isExampleRuns?How you find it
Syntax errorBreaks the language's grammarprint("Hi"No, not at allThe IDE (Thonny) tells you before it runs
Runtime errorSomething impossible while running10 / 0, int("hi"), scores[10]Starts, then crashesAn error message with a line number
Logic errorRuns fine, wrong answer>= instead of >Yes: the sneakiestOnly 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)
  1. Python tries the lines under try:.

  2. The user types ten. int("ten") can't work, so Python raises a ValueError.

  3. Instead of crashing, it jumps straight to except ValueError: and prints the friendly message. The break is skipped.

  4. The loop goes round. The user types 14. int("14") works, break leaves the loop.

  5. Output: Next year you'll be 15. The program never crashed: it is robust.

ExceptionCaused by
ValueErrorRight type, wrong value: int("ten")
ZeroDivisionErrorx / 0
IndexErrorA list index that doesn't exist
FileNotFoundErrorOpening a missing file with "r"
NameErrorUsing a variable that doesn't exist (often a scope problem)
!Watch out

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.

People think

The first item of a list is scores[1].

Actually

It's scores[0]. The last item of a list of 4 is index 3, not 4.

People think

word[0:3] gives 4 characters, 0 to 3.

Actually

It stops before 3: indexes 0, 1, 2. That's 3 − 0 = 3 characters.

People think

Parameters and arguments are the same thing.

Actually

Parameter = the name in the def line. Argument = the value passed in the call.

People think

print and return do the same job.

Actually

print shows a value to a person. return hands it back to the program to use.

People think

A variable made inside a function can be used in the main program afterwards.

Actually

It's local: wiped when the function ends. Return it instead.

People think

Valid data is correct data.

Actually

Validation only checks it's reasonable. Verification checks it was typed right. Neither proves it's true.

People think

Boundary data means silly values.

Actually

Boundary = the edges (0, 50 and −1, 51). Silly values are erroneous.

People think

Opening a file with "w" adds to it.

Actually

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

[ {"q":"In [\"a\", \"b\", \"c\"], the index of \"c\" is…","o":["3","2","c"],"a":1,"w":"Counting starts at 0: a = 0, b = 1, c = 2."}, {"q":"word = \"Python\". What is word[1:4]?","o":["\"Pyth\"","\"yth\"","\"ytho\""],"a":1,"w":"Start at index 1 (y), stop **before** index 4 (o): y, t, h. That's 4 − 1 = 3 characters."}, {"q":"In def area(w, h): … and a = area(4, 3), which are the **arguments**?","o":["w and h","4 and 3","area and a"],"a":1,"w":"Arguments are the actual values passed in the call. w and h are parameters."}, {"q":"A variable made inside a function is…","o":["Global: usable anywhere","Local: only exists inside it while it runs","Saved to a file"],"a":1,"w":"Local variables are wiped when the function returns."}, {"q":"Testing a 0–50 range with 50 is…","o":["Normal data","Boundary data","Erroneous data"],"a":1,"w":"It's right at the edge of what's allowed."}, {"q":"Typing your new password twice so they can be compared is…","o":["Validation","Verification","Authentication"],"a":1,"w":"Verification checks data was entered correctly. Double entry is one method."}, {"q":"open(\"log.txt\", \"w\") on a file that already holds 100 lines…","o":["Adds to the end","Wipes it first","Gives an error"],"a":1,"w":"\"w\" empties the file before writing. Use \"a\" to append."}, {"q":"The program runs but gives the wrong answer. That's a…","o":["Syntax error","Runtime error","Logic error"],"a":2,"w":"The code is valid and doesn't crash; the thinking is wrong. Only testing finds it."} ]