Write Python with a pen: no autocomplete, no running. Five short lessons: run a program in your head, draw names and objects, memorise the skeletons, catch the slips an interpreter would catch, and mark your own work.
What you need first
You have run one small Python program before. If not, read the Python first-steps guide first. This guide starts at the point where no computer is there to help.
A pen or pencil, an eraser, and squared or lined paper. Tables are easier on squared paper.
About 20 minutes for each lesson. Do the check questions on paper, not in your head.
01
Read like a computer
Goal You can run a short Python program by hand, one line at a time, and write down every name and value it uses.
When you write code on paper, nothing tells you that a line is wrong. Your only check is to run the program yourself, half in your head and half on the page. This is called a dry run: you act as the computer, with a pen, one line at a time.
A computer has no idea what your program is for. It does exactly what each line says, in order, and it remembers only what is stored under names. In Python, a name is a label for a value, such as count or items. While you trace, do the same. Do what the line says, not what you meant it to say.
The tool is a trace table. A trace table has one column for each name and one row for each line that runs. Build it in four steps.
1. Number the lines of the program in the margin, starting at 1. 2. Draw the table. The first column is Line. Then one column for each name. The last column is Note, where you write what happened, for example "test is true", or what the program printed. 3. Run the program. Python starts at line 1 and goes down. Each time a line runs, add a row. Write the line number, then the value of every name after that line has finished. Copy unchanged values down from the row above, so that every row can be read on its own. 4. Write a dash (–) for a name that does not exist yet. In Python, a name appears the first time something is stored under it. Using a name before that stops the program with a NameError.
A line that starts with def only creates the function. The lines inside it do not run until somebody calls the function. So a def line gets one row, and its body gets rows later, at the call.
Python shows what belongs together by indentation, the spaces at the start of a line. Lines that are pushed in under a for, if or def belong to it. When you move back out to the left, you have left that block.
A loop line runs many times, so it gets many rows. For a loop of the form for x in items, add one row each time x takes the next item. Write the new value of x. When no items are left, add one last row to show that the loop ends. Unlike C or Java, the name x is still there after the loop, and it keeps the last item.
An if line gets one row. If its test is false, the next row is the first line after the indented block.
When the program calls a function, the function's parameters become new names that start with the values you passed in. Keep them in the same table and say in Note that you are inside the function. When it returns, write the returned value in Note.
New words
dry run
Running a program by hand, with a pen, without a computer.
trace table
A grid with one column per name and one row per line that runs.
name
A label for a value, such as count. A name appears the first time something is stored under it.
indentation
The spaces at the start of a line. They show which lines belong to a for, an if or a def.
None
The value that means "nothing here". A function with no return gives it back.
Set up first. Number the lines of code from 1: def count_big is 1, count = 0 is 2, for x in items is 3, if x > limit is 4, count = count + 1 is 5, return count is 6, and print is 7. Blank lines are not numbered here. Draw six columns: Line
items
limit
count
x
Note.
Line 1
items = –
limit = –
count = –
x = –
def creates the function count_big. Its body does not run yet. Python moves on to the next line of code, which is line 7.
Line 7
items = [3, 8, 5]
limit = 4
count = –
x = –
print needs the value of count_big([3, 8, 5], 4), so the call happens first. The parameters items and limit are new names for the values passed in. Note: inside count_big.
Line 2
count = 0
count is created with 0.
Line 3
x = 3
The for line takes the first item of items. Note: first item.
Line 4
x = 3
count = 0
3 > 4 is false. The indented line 5 is skipped.
Line 3
x = 8
count = 0
The for line takes the next item.
Line 4
x = 8
count = 0
8 > 4 is true. Go into the indented block.
Line 5
x = 8
count = 1
count = 0 + 1 stores 1.
Line 3
x = 5
count = 1
The for line takes the last item.
Line 4
x = 5
count = 1
5 > 4 is true.
Line 5
x = 5
count = 2
count = 1 + 1 stores 2.
Line 3
x = 5
count = 2
No items are left, so the loop ends. x is still there and keeps the last item, 5.
Line 6
x = 5
count = 2
return count sends 2 back to the call. Note: returns 2.
Line 7
Back in the main program, print writes 2. The program prints 2.
Common mistakes
Wrong
for i in range(1, 4):
print(i)
range(1, 4) stops before 4. It gives 1, 2 and 3. If you want 1, 2, 3 and 4, the second number is one more than the last value you want: range(1, 5). Write the values of i down in a row, and the last one is visible at once.
Right
for i in range(1, 5):
print(i)
Wrong
for i in range(7 / 2):
print(i)
In Python, / always gives a float, so 7 / 2 is 3.5, and range cannot count with a float. It stops the program with a TypeError. // divides and rounds down to a whole number: 7 // 2 is 3.
Right
for i in range(7 // 2):
print(i)
Wrong
for i in range(3):
print(i)
print("done")
Both print lines are pushed in, so both belong to the loop. The word done is printed three times, once per round. If you want it once, at the end, move it back out to the left, level with the for.
Right
for i in range(3):
print(i)
print("done")
Wrong
def add(a, b):
result = a + b
print(add(1, 2))
A function without a return line gives back None. The sum is worked out and then thrown away when the function ends. The row for the call in your trace table would show Note: returns None. Write return result.
Right
def add(a, b):
result = a + b
return result
print(add(1, 2))
Check yourself
Make a trace table for these four lines, numbered 1 to 4: a = 4, then b = a, then a = a + 1, then b = b * 2. What are a and b at the end?
Try it first, then show the answer
Line 1: a = 4, b = –. Line 2: a = 4, b = 4. Line 3: a = 5, b = 4. Line 4: a = 5, b = 8. At the end a is 5 and b is 8. Line 3 changes a only. Line 4 uses b’s own value.
Predict, without running: what does for n in range(10, 0, -3): print(n) print? Write every value of n.
Try it first, then show the answer
It prints 10, 7, 4 and 1, each on its own line. The step is -3, and the next value, -2, is not above the stop value 0, so the loop ends.
In the worked program, what value goes in the x column on the row for line 6, and why?
Try it first, then show the answer
5. A Python loop variable stays after the loop and keeps the last item. The last item of [3, 8, 5] is 5.
02
Draw names and objects
Goal You can draw the names and objects of a Python program at any line, say what = does, and tell mutable objects from immutable ones.
In Python, a name is not a box that holds a value. A name is a label stuck on an object, like a label on a jar. Two labels can be stuck on the same jar. You cannot see that in the code, so you have to draw it. Use the same picture every time.
Draw two columns: Names on the left and Objects on the right.
Objects. Every value is an object: the number 5, the text "hi", the list [1, 2]. Draw each object as a box, and write its type and value, such as int 10 or str "hi". Give each object a label, O1, O2, O3, in the order it is made. A list is a box with one cell for each item. A cell that holds a number or a piece of text may show the value directly. A cell that holds a list or another object that can change has an arrow to it.
Names. A name is a label on the left with exactly one arrow to an object. A name has no value of its own. The names inside a function are drawn in a frame, a box around them, that exists only while the function runs.
What = does. The statement b = a never copies anything. It puts a second label, b, on the object that a already points at. The statement a = 5 finds or makes the object 5 and moves the arrow of a to it. Assignment moves arrows. It does not copy objects.
Immutable and mutable. An immutable object can never change after it is made. Numbers, strings, tuples and None are immutable. A mutable object can change in place. Lists, dicts and sets are mutable, and so are objects of your own classes.
With an immutable object, s = s + "!" does not touch the old string. It makes a new string and moves the arrow of s. Any other label on the old string still sees the old text. With a mutable object, b.append(3) changes the list itself. Every label on that list sees the change. Two names for one object is called aliasing.
Copying. b = a.copy() makes a new list with the same cells. It is a shallow copy: the cells hold arrows to the same items as before. If an item is itself a list, both outer lists share it. Draw the arrows and you will see this.
Function calls. A call makes a new frame. Its parameter names are new labels on the same objects the caller passed in. So a function can change a mutable argument in place. But a statement such as param = something only moves the label inside the frame.
Equal and same. a == b asks whether two objects look equal. a is b asks whether the two names are on the same object.
Garbage. An object with no label and no arrow into it can never be reached again. Python removes it. Cross it out in the picture.
New words
object
A value in memory, such as 5, "hi" or [1, 2]. Every value in Python is an object.
name
A label stuck on an object. It points at the object and has no value of its own.
immutable
Cannot change after it is made: numbers, strings, tuples, None.
mutable
Can change in place: lists, dicts, sets, objects of your own classes.
alias
A second name for the same object.
shallow copy
A new outer object whose cells lead to the same items as the old one.
frame
The box around the names of one function call. It exists only while the call runs.
a = [1, 2]. Draw two columns: Names and Objects. Make the list O1 with cells 1 and 2, and a label a with an arrow to O1.
b = a. Nothing new is made. Stick a second label, b, on O1. One list, two names.
b.append(3). The method changes O1 itself: it now has cells 1, 2 and 3. Both a and b lead to O1, so both see [1, 2, 3].
c = a.copy(). A new list O2 with cells 1, 2, 3 is made, and c leads to O2. Then c.append(4) changes only O2, which becomes [1, 2, 3, 4]. O1 is unchanged.
x = 10. Make the int O3 holding 10 and a label x with an arrow to it. Then y = x puts the label y on the same O3.
y = y + 1. First y + 1 is worked out and gives the int 11. Draw it as a new object, O4. (Python may reuse an 11 that already exists, but numbers never change, so you cannot tell the difference.) Then the arrow of y moves to O4. O3 still holds 10 and x still leads to it. A number is never changed in place.
nested = [a, [5]]. Make O5, a list with two cells. Cell 0 gets an arrow to O1 (the very list that a leads to). Cell 1 gets an arrow to a new list O6 holding 5. The label nested leads to O5.
dup = nested.copy(). Make a new list O7 with two cells that hold the same two arrows: cell 0 to O1 and cell 1 to O6. This is a shallow copy: the items are shared. The label dup leads to O7.
dup[1].append(6). dup[1] follows the arrow in cell 1 of O7 to O6. append changes O6 to [5, 6]. Cell 1 of O5 also leads to O6, so nested shows [5, 6] too.
dup[0] = [0]. A new list O8 holding 0 is made, and the arrow in cell 0 of O7 moves to O8. This changes the cell of dup only: cell 0 of O5 still leads to O1.
The prints. a, b and c are [1, 2, 3], [1, 2, 3] and [1, 2, 3, 4]. x and y are 10 and 11. nested is [[1, 2, 3], [5, 6]] and dup is [[0], [5, 6]].
The aim was to keep a backup of the old list, but backup = scores only sticks a second label on the same list. When scores grows, backup shows [70, 80, 90] too. Make a separate list with scores.copy().
The default [] is made once, when the def line runs, and every call that leaves out items gets a label on that same list. So add_item(1) gives [1], and then add_item(2) gives [1, 2], not [2]. Use None as the default and make a fresh list inside the function.
Right
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
Wrong
grid = [[0] * 3] * 3
grid[0][0] = 1
[[0] * 3] * 3 repeats the arrow, not the row. The outer list gets three cells that all lead to the same inner list. Setting grid[0][0] changes that one row, and all three rows show it. Build each row separately with a comprehension.
Right
grid = [[0] * 3 for _ in range(3)]
grid[0][0] = 1
Wrong
s = "hi"
s[0] = "H"
A string is immutable, so a single letter cannot be changed in place. Python stops with a TypeError. Make a new string and move the label: s = "H" + s[1:].
Right
s = "hi"
s = "H" + s[1:]
Check yourself
Draw it: s = "hi", then t = s, then s = s + "!". What do s and t lead to at the end?
Try it first, then show the answer
After t = s, both labels are on the string O1 "hi". Then s + "!" makes a new string O2 "hi!", and the arrow of s moves to O2. t still leads to O1. So s is "hi!" and t is "hi". Strings are immutable, so t never changes.
Draw it: x = [1, 2], then y = [1, 2], then z = x. Which of x == y, x is y and x is z are True?
Try it first, then show the answer
There are two lists: O1, led to by x and z, and O2, led to by y. x == y is True, because both lists look equal. x is y is False, because they are different objects. x is z is True, because the labels are on the same object.
A function is defined as def f(v): followed by two lines, v.append(0) and then v = [9]. After a = [1] and f(a), what does print(a) show?
Try it first, then show the answer
It shows [1, 0]. Inside the call, v is a second label on the list a leads to, so v.append(0) changes that shared list. Then v = [9] only moves the label v inside the frame to a new list. The list that a leads to is unchanged by it.
03
Syntax to memorise
Goal You can write the eight Python skeletons below from memory, shortest first, with every colon and indent in place.
Some code cannot be worked out on the spot. You must know its shape without looking. The eight shapes in this lesson are the ones Python needs again and again. They are short enough to learn by heart. Learn them in this order, shortest first, and add the next one only when the last is solid.
1. def, with if, elif and else. 2. for. 3. f-strings. 4. List methods. 5. Dict methods. 6. Comprehensions. 7. try and except. 8. A class with __init__.
How to memorise: cover the page and write the shape on paper. Compare it with the page, character by character. Tick it only when it is right with no peeking. Do it again tomorrow. A shape that you write correctly three days in a row is learned.
The program below contains all eight shapes, each marked with a comment such as (2). Read it once. Then cover it and write each shape on its own, in a few lines of your own paper.
Two things that you must never forget. Every line that opens a block ends with a colon: def, if, elif, else, for, while, class, try, except. And every block is indented by the same amount, usually four spaces.
List methods. words.append(x) adds at the end. words.pop() removes the last item and gives it back. words.insert(i, x) puts x at position i. words.remove(x) removes the first x. words.sort() sorts in place and returns None. sorted(words) returns a new sorted list. len(words) is the length. words[0] is the first item, words[-1] the last, and words[1:3] is a slice: positions 1 and 2, because the end is not included.
Dict methods. d[key] = value stores. d[key] reads, and stops with a KeyError when the key is missing. d.get(key, default) reads and gives the default when the key is missing. key in d asks whether the key is there. d.keys(), d.values() and d.items() give the keys, the values and the (key, value) pairs. del d[key] removes.
New words
skeleton
The fixed shape of a piece of code, with the details left to fill in.
comprehension
A one-line way to build a list or dict from another collection, such as [n * n for n in range(4)].
f-string
A string with the letter f before the quote. Names and expressions inside { } are put into the text.
exception
An error that stops normal flow, such as ValueError. An except block can handle it.
self
The first parameter of a method. It stands for the object the method was called on.
Worked example
Python 3
# (1) def, with if, elif and elsedefaverage(nums):returnsum(nums)/len(nums)deflabel(score):ifscore>=8:return"high"elifscore>=4:return"mid"else:return"low"# (4) list methodswords=["pear","fig","apple"]words.append("kiwi")words.sort()last=words.pop()print(words,last,len(words))# (5) dict methods, and (2) forlengths={}forwinwords:lengths[w]=len(w)forkey,valueinlengths.items():# (3) f-stringprint(f"{key} has {value} letters")print(lengths.get("plum",0),"fig"inlengths)# (6) comprehensionssquares=[n*nforninrange(4)]long_words=[wforwinwordsiflen(w)>3]sizes={w:len(w)forwinwords}print(squares,long_words,sizes)# (8) class and __init__classCounter:def__init__(self,name):self.name=nameself.total=0defadd(self,amount):self.total+=amountc=Counter("steps")c.add(5)c.add(7)print(f"{c.name}: {c.total}, average {average([c.total,8]):.1f}, {label(c.total)}")# (7) try and excepttry:n=int("12a")exceptValueError:n=-1print(n)
Predict the output first, then show it
Expected output
['apple', 'fig', 'kiwi'] pear 3
apple has 5 letters
fig has 3 letters
kiwi has 4 letters
0 True
[0, 1, 4, 9] ['apple', 'kiwi'] {'apple': 5, 'fig': 3, 'kiwi': 4}
steps: 12, average 10.0, high
-1
Step by step
(1) def, with if, elif and else. def name(parameters): then the body, indented. return gives a value back. if test: ... elif test: ... else: ... Each of these lines ends with a colon, and the elif and else line up under the if. average(nums) divides the sum by the length. label(score) gives high for 8 or more, mid for 4 to 7, and low otherwise.
(2) for. for item in collection: with the body indented. for key, value in lengths.items(): takes a pair at a time and gives each part its own name. To count n times, use for i in range(n):, which gives 0 up to n - 1.
(3) f-string. f"text {name} more text {value}". The letter f comes right before the opening quote. Whatever sits inside the curly braces is worked out and put into the text. A format can follow a colon: {x:.1f} writes x with one decimal place. This line prints apple has 5 letters, and so on.
(4) List methods. words starts as pear, fig, apple. append("kiwi") adds kiwi at the end. sort() puts them in alphabetical order: apple, fig, kiwi, pear. pop() removes pear and gives it back, so last is pear and words is apple, fig, kiwi. The line prints ['apple', 'fig', 'kiwi'] pear 3.
(5) Dict methods. lengths = {} makes an empty dict. lengths[w] = len(w) stores a pair. lengths.get("plum", 0) gives 0, because plum is not a key. "fig" in lengths is True. The line prints 0 True.
(6) Comprehensions. [expression for item in collection] builds a list. Add if test at the end to keep only some items. {key: value for item in collection} builds a dict. The expression comes first. squares is [0, 1, 4, 9], long_words is ['apple', 'kiwi'], and sizes is a dict of lengths.
(7) try and except. try: with the risky code indented, then except ErrorName: with the plan B indented. int("12a") cannot read the text as a number, so it raises a ValueError, the rest of the try block is skipped, and the except block sets n to -1. The program prints -1.
(8) A class with __init__. class Name: then def __init__(self, parameters): which sets self.attribute = value. Every method has self as its first parameter. You leave self out when you call: c.add(5) calls add with self set to c. The program prints steps: 12, average 10.0, high.
Common mistakes
Wrong
words = ["pear", "fig"]
ordered = words.sort()
sort() changes the list in place and gives back None, so ordered is None. Either sort in place and keep using words, or ask for a new sorted list with sorted(words).
Right
words = ["pear", "fig"]
ordered = sorted(words)
Wrong
ages = {"Leon": 20}
age = ages["Emma"]
The key Emma is not in the dict, so d[key] stops the program with a KeyError. If a missing key is possible, use d.get(key, default), or ask key in d first.
Right
ages = {"Leon": 20}
age = ages.get("Emma", 0)
Wrong
words = ["pear", "fig"]
n = words.length
m = words.size()
Python has one way to ask how many: the function len, with the thing in brackets. Lists have no length field and no size method. Write len(words).
Right
words = ["pear", "fig"]
n = len(words)
Wrong
name = "Mia"
print("Hello {name}")
Without the letter f in front of the quote, this is an ordinary string, and the braces are printed as they are: Hello {name}. Nothing is reported as an error. Write f"Hello {name}".
Right
name = "Mia"
print(f"Hello {name}")
Check yourself
On paper, write from memory a function longest(words) that returns the longest string in a non-empty list, using a for loop.
Try it first, then show the answer
def longest(words): then, indented, best = words[0] then for w in words: then, indented again, if len(w) > len(best): then best = w and finally, back at the first indent, return best. Check: colons on def, for and if; the same indentation inside each block; return outside the loop.
Write one line that builds a list of the squares of the even numbers from 0 to 8, using a comprehension.
Try it first, then show the answer
[n * n for n in range(9) if n % 2 == 0] It gives [0, 4, 16, 36, 64]. The expression n * n comes first, then the for, then the if.
Write a class Dog whose __init__ takes a name and stores it, and a method speak that returns the text name says woof, using an f-string.
Try it first, then show the answer
class Dog: then, indented, def __init__(self, name): then self.name = name then, back one level, def speak(self): then return f"{self.name} says woof" Check: self is the first parameter of both methods, and self.name is used to read the attribute.
04
Common handwriting mistakes
Goal You can read your own Python answer a second time, as the interpreter would, and catch the slips that would otherwise cost marks.
On a computer, Python stops you at the first syntax error and tells you the line. On paper, nobody stops you, and every one of these slips costs marks even though it has nothing to do with whether you understand the problem. So you must be your own interpreter.
When your answer is finished, read it a second time. This time do not ask whether the logic is right. Ask only whether the form is right. Use this list, and tick each item with your pen.
1. Every line that opens a block ends with a colon: def, if, elif, else, for, while, class, try, except. 2. The lines of a block are indented by the same amount. Lines after the block move back to the left. 3. Every ( [ { has its partner, and every opening quote has a closing quote of the same kind. 4. = stores a value. == asks a question. There is no ++: write += 1. 5. Use and, or, not. Use elif, not else if. Write True, False and None with a capital letter. 6. Names are case sensitive: count and Count are different names. 7. Every method of a class has self as its first parameter, and reads and writes attributes with self. 8. A function that should give a value back has a return. Without one, it gives back None. 9. Text and numbers do not mix: "5" + 1 is an error. Convert with int() or str(). 10. Python does not tell you off for a name you misspelled until the line runs. Check each name against where it was first stored.
Below are four pairs that cost the most marks. In each pair the first block is what people write, and the second block is the repair.
Common mistakes
Wrong
def double(x)
return x * 2
for i in range(3):
print(i)
Two slips. The def line has no colon at the end. And the print line is not indented, so the for has no body. Python refuses both. Put a colon after every line that opens a block, then indent the lines under it by four spaces.
Right
def double(x):
return x * 2
for i in range(3):
print(i)
Wrong
if x > 0 && y > 0:
count++
else if x == 0:
print("zero")
Three habits from C and Java. Python spells && as and. It has no ++, so write count += 1. And else if is written elif. Python reports a syntax error on the first of these.
Right
if x > 0 and y > 0:
count += 1
elif x == 0:
print("zero")
Wrong
if x = 3:
print("three")
if x == 3 or 4:
print("three or four")
Two slips. A single = stores, and it is not allowed in a condition, so Python reports a syntax error. And x == 3 or 4 means (x == 3) or 4. The 4 is always true, so the second print always runs. Write each comparison in full, or use x in (3, 4).
Right
if x == 3:
print("three")
if x == 3 or x == 4:
print("three or four")
Wrong
class Dog:
def __init__(name):
self.name = name
def rename(self, new):
name = new
Two slips. __init__ must take self as its first parameter, or Python cannot pass the new object in, and new Dog("Rex") fails. And inside rename, name = new makes a local name that disappears when the method ends. The attribute is self.name.
Right
class Dog:
def __init__(self, name):
self.name = name
def rename(self, new):
self.name = new
Check yourself
Fix both slips: else if x == 3 print("three") (two slips: the keyword and a missing symbol).
Try it first, then show the answer
It must be elif x == 3: and then the print goes on the next line, indented: print("three"). So elif replaces else if, and a colon is added after the condition.
Fix the loop header: for i in range(len(items): total += items[i]
Try it first, then show the answer
The brackets are not paired: range( and len( are both opened, but only one is closed. Write for i in range(len(items)): and put total += items[i] on the next line, indented. Counting brackets in pairs finds it.
In a class Dog, the method __init__(self, name) contains the line name = name. What is wrong, and what should it say?
Try it first, then show the answer
It stores the parameter under the same local name, so nothing is kept after the method ends. The attribute needs self: self.name = name.
05
Mark yourself
Goal You can mark your own paper answer honestly against a rubric, find where the marks went, plan tomorrow's practice, and write under time.
On paper, a page of writing can look finished and still be wrong. Marking yourself fixes that. You compare your answer with a rubric. A rubric is a list of lines, each worth a number of marks, each describing something that you can see in the answer. Be strict. The mark that counts is the one a stranger would give.
How to read a rubric, line by line.
1. Read one line. Ask: what exactly would I see in a correct answer? 2. Find that thing in your answer and touch it with the tip of your pen. If you can point to it, tick the line and write its marks. If you cannot point to it, the line is worth zero. 3. Do not give half marks for "nearly". Give part of a line only if the rubric says how it splits. 4. Check every path. A line such as "handles the empty string" is zero if only the normal case works. 5. Add up the lines. Write the total as marks out of the maximum on the page.
How to find where the marks went. Put each lost mark into one of four boxes. Idea: I did not know how to solve it. Recall: I knew, but could not remember the syntax or the method name. Slip: a small mistake in something I did know, such as a missing colon or a missing return. Time: I ran out of time. Count each box. The biggest box tells you what to work on. Idea: reread the lesson. Recall: more cover-and-write practice. Slip: the interpreter pass from the last lesson and a tiny test. Time: practise planning.
What to do the next day. First, rewrite the same answer on a blank page without looking. Second, do a different question of the same kind. Third, mark both again. Repeat after three days and after a week. When the biggest box changes, you are improving.
How to write under time.
1. Plan first. Use the first tenth of the time. Write the def line and three or four comment lines that name the steps. Do not skip this. 2. Leave a blank line between blocks of code. You will need room to insert a line you forgot. Python is sensitive to indentation, so write the indentation as clearly as you can, and use the same four spaces each time. 3. Name things clearly: count, not c; longest, not l. Clear names stop you from mixing up two names. 4. Test with a tiny input, by hand. When the code is written, run it on the smallest case: one item, an empty list, a one-letter word. Then run one case that repeats a value. This takes a minute and finds most boundary errors. 5. Spend about one minute per mark. If you are stuck on a part, write a comment saying what you would do, move on, and come back. 6. Leave two minutes for the interpreter pass.
On the Practice page, the questions of kind Write code have a Paper mode with a timer, a model answer and a rubric with tick boxes. Use it exactly like this lesson: write on paper, then mark.
New words
rubric
A list of lines, each worth some marks, each describing something you can point to in the answer.
boundary case
A test input that sits on an edge, such as one item, an empty string, or a repeated value.
interpreter pass
A second reading of your answer that checks only the form: colons, indents, brackets, self.
The task: write count_letters(word), which returns a dict that says how many times each letter occurs in word. The rubric has four lines, one mark each. (1) The def line is right: the name, one parameter, and a colon. (2) It starts with an empty dict. (3) It visits every letter and adds 1 to that letter’s count, including the first time the letter is seen. (4) It returns the dict.
Mark line 1. def count_letters(word): is right. Put the pen on it. +1.
Mark line 2. counts = {} is there. +1.
Mark line 3. for ch in word visits every letter, and counts[ch] = counts.get(ch, 0) + 1 adds 1, using 0 when the letter is new. +1.
Mark line 4. The pen has nothing to point at: there is no return line. 0 marks.
Score: 3 out of 4. Now the tiny test: the word "aba". The dict is built correctly inside, but the function ends without return, so the call gives back None. The program prints None (expected {'a': 2, 'b': 1}). The lost mark goes in the Slip box: the idea was right, one line was left out. The fix is return counts, indented once, level with the for.
Common mistakes
Wrong
counts = {}
for ch in word:
counts[ch] += 1
The first time a letter is seen, counts[ch] does not exist, so counts[ch] += 1 stops with a KeyError. The tiny test "a" finds it at once. Start from a default: counts[ch] = counts.get(ch, 0) + 1.
Right
counts = {}
for ch in word:
counts[ch] = counts.get(ch, 0) + 1
Wrong
total = 0
for i in range(1, len(items)):
total += items[i]
range(1, len(items)) starts at position 1, so items[0] is never added. A tiny test with one item shows it: the total stays 0. Start at 0, or skip the positions and loop over the items themselves.
Right
total = 0
for x in items:
total += x
Wrong
def total(items):
s = 0
for x in items:
s += x
print(s)
The task said the function returns the total, but this one prints it and returns None. The caller gets nothing to use. This is a reading slip: before you write, find the words returns, prints and reads in the task and underline them.
Right
def total(items):
s = 0
for x in items:
s += x
return s
Check yourself
A rubric line says "gives the right answer for the empty string". Your function works for "aba" but crashes on "". How many marks does that line get, and why?
Try it first, then show the answer
Zero. The line is about a path, and that path fails. Put the pen on the code that handles the empty string. There is none that works. Test every path with a tiny input, not only the usual one.
You have 12 minutes for a 12-mark task: write a class with __init__ and one method, and a few lines that test it. Write a plan with times.
Try it first, then show the answer
1 minute: write the class line and the two def lines as comments. 4 minutes: write the class. 2 minutes: write the test lines for one tiny case. 3 minutes: run that case by hand with a trace table. 2 minutes: the interpreter pass for colons, indents and self.
Write the tiny tests that you would run by hand on a function that returns the most common letter in a word.
Try it first, then show the answer
The empty string (decide what to return, and check the code does it), a one-letter word, a word where two letters tie (which one wins?), and a word like "aab" with a clear winner. Run each through your code with a trace table.