← back to CS 115
Week 6Guttag §Chapter 5241 min full read
7 concepts19 worked examples26 exercises4 exam-level7 figures
What are you here for?

06 Structured Types, Mutability, and Higher-Order Functions (Chapter 5)

Start with this

One question before you read anything. Getting it wrong is the point: it shows you what this section is for.

§06.0 — a word you already know how to slice

Three questions before the new material, to find out which old part you should reread first. Not knowing them is not a problem; each one says where to look. Here is the first, on a string, which is the you already have.

word = 'structured'
print(word[0:4], word[-3:], word[4])
print(len(word), word[0] + word[-1])
Find(a) Write the two lines this prints.
Given
  • The word has ten characters.

  • starts at 0 and a negative index counts back from the end.

IPython console
Hint 1/4

Each of the five things being printed is either a slice, a single character, or a length. Do them one at a time and write the pieces down before joining them with spaces.

Hint 2/4

s[a:b] stops before b, so it has b minus a characters. s[-3:] is the last three. print puts a single space between its arguments.

Hint 3/4

The word is structured, so index 0 to 3 is s, t, r, u and index 4 is the fifth character. Its length is 10 and word[-1] is the last character.

Hint 4/4

The first line is three items separated by spaces and the second is a number and a two letter string.

Show solution

Take the first line apart

$$\texttt{word[0:4]}\;\rightarrow\;\texttt{stru}$$

Indices 0, 1, 2 and 3, because a slice stops before its second number.

$$\texttt{word[-3:]}\;\rightarrow\;\texttt{red}$$

No second number means all the way to the end, and minus three is three back from it.

$$\texttt{word[4]}\;\rightarrow\;\texttt{c}$$

A single index gives one character, not a string of length four.

Then the second line

$$\texttt{len(word)}\;\rightarrow\;10$$

Ten characters, so the valid indices are 0 to 9.

$$\texttt{word[0] + word[-1]}\;\rightarrow\;\texttt{sd}$$

Both are strings, so + joins them; nothing here is a number.

Answer $$\boxed{\texttt{stru red c}\;/\;\texttt{10 sd}}$$
Check

Count the letters of the word on your fingers: s t r u c t u r e d is ten, the fifth is c, and the last three are r e d.

What this looks like in Spyder
The Variable Explorer tab showing a table with Name, Type, Size and Value columns for the names the program created.

The tab next to Help. It every name your program left behind with its type and its size, so it answers the one question a listing cannot: what is in this name right now. The stepper on this page shows the same four columns.

Spyder 5.5.1 on Linux, running the program in its own editor. A different version moves a few toolbar icons; the panes are named the same.

A program keeps the marks of a lab group in order to report them two ways, so before sorting them it saves a copy under a second name. It sorts, prints the sorted marks, then prints the copy to show the original order. The copy comes out sorted too, and nothing in the program ever told it to change.

By the end of this section you can take any short program built from , , lists, dictionaries and functions passed as values and write down exactly what it prints, including the word None and the order the come out in; and you can say, for every operation the course gives you, whether it changes the thing you handed it or builds a new one.

In 60 seconds

Four containers arrive at once, and the only question that matters about each of them is whether an operation rewrites the thing you already have or builds a new one. Tuples and ranges cannot be rewritten, lists and dictionaries can, and a function is itself a value you can store and pass.

A name holds a reference, not a copy
$$\texttt{b = a}\;\Longrightarrow\;\text{one object, two names}$$

Every assignment of a list or a dictionary. Changing the object through either name is visible through both, because there is only one object.

Mutating methods hand back None
$$\texttt{L.append(e)},\ \texttt{L.sort()},\ \texttt{L.reverse()}\;\Longrightarrow\;\texttt{None}$$

Any line of the form name equals L dot something. If the method changes the list, the value of the call is None and the name you assigned to is now useless.

Immutable means the slots, not the contents
$$\texttt{t = (1, [2, 3])}\;\Longrightarrow\;\texttt{t[1].append(4)}\;\text{works}$$

A tuple that holds a list. You cannot replace the list in the slot, and you can change that list as much as you like.

A key is a name for a value, not a position
$$\texttt{d[k]}\;\text{looks up}\;k,\;\text{never the}\;k\text{th item}$$

Every dictionary. A missing key is a rather than a silent wrong answer, so test with in or use get with a fallback.

Three most common mistakes
  1. Writing sorted_marks = marks.sort() and then using sorted_marks. The list really is sorted, and the name holds None, so the next line that indexes it stops the program.

  2. Making a backup with backup = marks and expecting it to survive a change to marks. It does not: that line made a second name for one list, and the copy that does survive is marks[:] or list(marks).

  3. Removing items from a list inside a for loop over that same list. The loop counts positions while the positions move underneath it, so items get skipped, and the bug hides because on many inputs the answer still comes out right.

Labs are 20 per cent of the course mark, the midterm 40 and the final 40. On the one past midterm paper that was read while writing this page, the single heaviest question was worth 30 marks and consisted of four short programs to be traced by hand; two of its four parts were about exactly this week's material, one being a list indexed by its own contents inside a while loop and one being a tuple that held a list. That is one paper rather than a rule, but tracing is the cheapest 30 marks on it and this week is where tracing gets hard.

How much time do you have?
10 minutes

The two facts that decide most of the marks: which list operations change the list and hand back None, and why two names can be one list.

The 60-second card · Lists, and the two families every list operation belongs to · Two names, one list · Formula card
45 minutes

Everything a lab answer needs: a tuple to hand back two values, the list methods with their return values, cloning, and a dictionary built key by key.

The 60-second card · Tuples: one value that carries several · Lists, and the two families every list operation belongs to · Two names, one list · Dictionaries · Scaffolding comes off · B · computation
full read

Adds the three parts that the lecture spends time on and that the exam has used: ranges as a sequence you never store, a function passed to another function or used as a default value, and the rules for changing a structure while you are walking through it.

The 60-second card · Recall first · Conventions · Tuples: one value that carries several · Ranges · Lists, and the two families every list operation belongs to · Two names, one list · Dictionaries · Functions are values too · Changing a structure while a loop is walking through it · Method boxes · Look-alike pairs · Scaffolding comes off · Full exam-style question · A · concept · B · computation · C · exam level · D · interleaved · Mistake ledger · Formula card · Check yourself
By the end of this section
  1. Return more than one value from a single function by building a tuple, and read values back out of a tuple or a nested tuple by index and by slice.

  2. Predict what a range prints, how long it is, which values it contains and when two ranges written differently are equal.

  3. Classify every list operation the course gives you as one that changes the list and hands back None, or one that leaves it alone and hands back a new value, and say what each returns.

  4. Draw the names and the objects for a program that assigns, clones and passes a list, and use the drawing to say which prints change after a call.

  5. Build a dictionary key by key from data, including a dictionary whose values are lists or tuples, and handle a missing key without stopping the program.

  6. Pass a function to another function, store functions in a structure and use a function as the default value of a parameter, and tell passing a function apart from calling it.

  7. Rewrite a loop that removes items from the structure it is walking through, using one of the three safe patterns, and say what the unsafe version prints instead.

Syllabus coverage

Structured Types — covered

Objects with parts you can reach: strings as the one you already know, then tuples, ranges, lists and dictionaries. Creating them, indexing, slicing, length, membership with in, walking them with for, nesting one inside another, and the operations the four of them share as against the ones only some of them have.

Mutability — covered

Which of the four can be rewritten and which cannot, what a name really holds, aliasing and side effects, cloning with a full slice or with list, the difference between is and equals equals, what happens to a list passed as an argument, and the rules for changing a structure while a loop is walking it.

Higher-Order Functions — covered

A function name as a value that can be stored in a name, put in a list or a dictionary, passed as an argument and used as the default value of a parameter, and a function that takes another function as a parameter and applies it to every item of a list.

Chapter 5 — covered

The whole of the chapter on structured types, mutability and , read as four containers and one question about each of them.

The chapter also introduces list comprehensions and the built in higher order functions map and filter. The lecture for this week does not use them and the lab sheet says only material covered in the course may be used, so they are named here and not taught, and no solution on this page uses one.

Tables as lists of lists — deferred

A table built as a list whose items are lists, reached with two index brackets and walked with two nested loops.

The lecture slides for this week end with tables, and the syllabus gives multi dimensional structures a week of their own straight after this one. Everything needed for them is on this page, which is why the next section can start from the second bracket and nothing here needs it.

Recall first
Indexing and slicing a string

For a string s, s[0] is the first character, s[-1] the last, and s[a:b] the characters from index a up to but not including b. len(s) is how many characters there are, so the last index is len(s) - 1.

Tuples, lists and ranges use the same two notations with the same rules, so everything you already do to a word works on a list of marks. The only new part is that a list also lets you assign to L[i].

A string is immutable and its methods return new strings

name.strip() does not shorten name. It builds and hands back a new string, and unless you write name = name.strip() the old one is still there with its spaces.

This is the same distinction the whole section turns on, met once already. Half of the list methods behave like strip and hand back something new; the other half do the opposite and change the list in place.

for with range, and while with a counter

for i in range(a, b): gives i the values a up to b minus 1. A while loop needs three parts you write yourself: something set up above it, a test, and a change inside the body to whatever the test reads.

Walking a structure by position uses for i in range(len(L)):, and the safe way to remove items while walking uses a while whose counter only advances sometimes. That second pattern is the point of the last concept here.

Functions, return, and the docstring as a contract

def name(p1, p2): creates a name; the body runs only when the name is called. A body that reaches its end without a return hands back None. The docstring says what the function assumes and what it returns.

Every exercise on this page is a function, and the new questions are about what the contract has to promise now that arguments can be changed by the body.

is new, and it is what turns a line of a file into pieces

'a,b,c'.split(',') hands back ['a', 'b', 'c'], a list of strings. With no argument, split() cuts on runs of whitespace instead. It is a string method, and it appears here because until this week there was nothing for it to hand back.

It is the join between the strings and files of the earlier sections and the structures of this one, and it is how almost every lab question reads its data.

Try it yourself first (2 questions)
1§06.0 — a method whose answer you forgot to keep

The second one. A name holds a string with spaces around it, and the program tries to tidy it.

name = '  Ada  '
name.strip()
print('[' + name + ']')
name = name.strip()
print('[' + name + ']')
Find(a) Write the two lines this prints.
Given
  • name starts as two spaces, the letters Ada, two spaces.

  • The square brackets in the output are printed characters, put there so that the spaces can be seen.

IPython console
Hint 1/4

Ask what the second line of the program did with the value that strip handed back, and where that value went.

Hint 2/4

A string cannot be changed. Every string method builds a new string and hands it back, and a call whose value is not assigned anywhere is thrown away.

Hint 3/4

Here the bare call is name.strip() on line 2 with nothing on its left, and the assignment name = name.strip() only happens on line 4.

Hint 4/4

The first print still shows the spaces and the second does not.

Show solution

Read the bare call for what it is

$$\texttt{name.strip()}$$

A call on a line of its own. Its value has nowhere to go, so it is discarded.

$$\texttt{name}\;\text{unchanged}$$

Strings are immutable, so there is no version of this where the call edits the original.

Then the assignment

$$\texttt{name = name.strip()}$$

Now the new string is bound to the old name, which is the only way the change survives.

Answer $$\boxed{\texttt{[ Ada ]}\;\text{then}\;\texttt{[Ada]}}$$
Check

Check it the other way round: if the bare call had changed the string, the fourth line would be pointless and both printed lines would be the same.

2§06.0 — a loop that eats a number one digit at a time

The last one. A while loop walks the digits of a whole number from the right, using the two operators from the numerical programs section.

n = 4236
total = 0
while n > 0:
    total = total + n % 10
    n = n // 10
print('digit sum', total)
print('n is now', n)
Find(a) Write the two lines this prints.
Given
  • n starts at 4236.

  • % gives the remainder and // throws away the remainder.

IPython console
Hint 1/4

Two separate questions: what the digits add up to, and what is left in n when a loop with this test finally stops.

Hint 2/4

n % 10 is the rightmost digit and n // 10 is the number without it. The loop runs while n is above 0.

Hint 3/4

With n starting at 4236 the digits come off in the order 6, 3, 2, 4, and n goes 4236, 423, 42, 4 and then 0.

Hint 4/4

The digits add to 15 and the loop can only stop once n has been emptied.

Show solution

Walk the four passes

$$6 + 3 + 2 + 4 = 15$$

The remainders come off right to left, one per pass.

$$4236 \to 423 \to 42 \to 4 \to 0$$

Floor division by ten drops a digit each time, and the test fails only at 0.

Answer $$\boxed{\texttt{digit sum 15}\;/\;\texttt{n is now 0}}$$
Check

Independent check on the sum: 4236 has digits 4, 2, 3, 6, and 4 plus 2 is 6, plus 3 is 9, plus 6 is 15, added in the opposite order to the loop and giving the same total.

Notation
symbolreads asmeanswatch out
$\texttt{(1, 'two', 3)}$

a tuple of three items

An ordered group of values written with round brackets. The items may be of different types and may themselves be tuples or lists.

The brackets are not what make it a tuple, the commas are. A single item needs a trailing comma, so (5,) is a tuple and (5) is just the number 5.

$\texttt{[2, 8, 3]}$

a list of three items

An ordered group of values written with square brackets, and the only sequence in this section whose items can be replaced one by one.

Square brackets do two unrelated jobs. Around a group of values they build a list; after a name they index into one.

$\texttt{\{'Ada': 91\}}$

a dictionary from Ada to 91

A group of key and value pairs written with curly braces. The key is what you look a value up by.

Curly braces with nothing in them, {}, make an empty dictionary. There is no such thing as an empty list written that way.

$\texttt{s[i]}$

the item of s at index i

For a string, tuple, list or range, the item at position i, counting from 0. For a dictionary, the value stored under the key i.

Two different jobs behind one notation. On a sequence an index past the end is an , on a dictionary a key that is not there is a KeyError.

$\texttt{s[a:b]}$

the slice of s from a up to b

A new object holding the items from index a up to but not including index b. Slicing a tuple gives a tuple, slicing a list gives a list.

A slice always builds something new, which is why L[:] is the cheapest way to clone a list. An index out of range in a slice is not an error, it is silently trimmed.

$\texttt{x in s}$

x is in s

True when x is one of the items of the sequence s, and for a dictionary, True when x is one of its keys.

On a dictionary it looks at keys only. Asking whether a value is in a dictionary with this needs x in d.values().

$\texttt{L.append(e)}$

append e to L

Adds one item to the end of the list L, changing L itself.

It hands back None. L = L.append(e) throws the list away and leaves the name holding nothing.

$\texttt{L[:]}\;\text{and}\;\texttt{list(L)}$

a clone of L

Two ways of building a new list with the same items, so that changing one list does not change the other.

The new list holds the same item objects. If an item is itself a list, that inner list is still shared by both.

$\texttt{a is b}$

a is the same object as b

True only when the two names refer to one and the same object, whatever it contains.

Not a test of contents. Two different lists with equal items give True for == and False for is.

$\texttt{f}\;\text{against}\;\texttt{f(x)}$

the function itself against a call of it

A name with no brackets after it is the , a value like any other. With brackets it is a call and the whole thing stands for what the call handed back.

Passing f() where f was wanted calls the function too early, usually with the wrong number of arguments, and the error message says the type of the result is not callable.

Conventions used here
Every output on this page came out of an interpreter.

No block of output here was predicted by eye. Each program was written to a file, run, and the characters it wrote were copied back in, which is why some of them look odd rather than tidy: a line that says None where an answer was expected, a slice of a range that reports a stop it never reaches, a run that ends in a traceback.

In a programming course the printed answer is the whole claim, so the page cannot itself be guessing.

What this page is allowed to use, and what it waits for.

Everything here is built from what the course has covered by the end of this week: numbers, text, True and False, input, print, if, while, for, range, def and return, the string operations, files, and from now on tuples, ranges, lists and dictionaries with the methods the exam paper lists on its cover.

The lab sheet says in as many words that only functionality covered in the course may be used, and a solution that reaches forward teaches you to write something this week's lab will not accept.

How this page reports what an operation does.

For every operation there are two separate questions, and both are always answered: does it change the object you handed it, and what does it hand back. So L.sort() is written as changes L, hands back None, while sorted(L) is leaves L alone, hands back a new list.

The cover sheet of the exam gives you the names of the methods, so the names are not what is being tested.

The order the keys of a dictionary come out in.

On the Python this course uses, a dictionary keeps the order in which each key was first added, and every dictionary printed on this page shows that order rather than a sorted one. The lecture still advises not to lean on it, and this page follows that advice in one direction only: where an answer has to be in a fixed order, it walks sorted(d.keys()) instead.

A tracing question asks for the exact characters, and refusing to say what the order is would leave you unable to answer it.

A Sample Run shows what the user typed.

Where a program calls input, the Sample Run shows the prompt and then, on the same line, the characters the user typed, exactly as a lab sheet does. The terminal echoes what is typed, which is why those characters are part of the picture even though the program never printed them.

The lab sheets are marked against a Sample Run in this shape, so an exercise here is only in lab format if it carries one.

Where the word immutable is used, it is about the slots.

A tuple, a range and a string are called immutable on this page, and the claim is precise: you cannot replace an item of one, and you cannot make one longer or shorter. It is not a claim about the objects inside.

The commonest wrong sentence in this part of the course is that a tuple cannot contain anything that changes, and it is wrong in a way that costs marks on a tracing question.

6.1Tuples: one value that carries several

A tuple is a fixed row of values, so one call can hand back two answers at once.

Everything so far could hand back exactly one thing: one number, one string, one True or False. The first new type lifts that limit.

Solvable with what we have
  • Return one number, such as the largest common divisor of two numbers.

  • Print several things at once with one print.

  • Keep two answers in two names inside the body.

Not solvable yet
  • Hand two numbers back from one call, ready for arithmetic.

  • Keep a course name and a mark together as one value.

  • Walk a group of mixed types with one loop.

The lab question wants both the smallest and the largest common divisor from one function. The loop is easy. The trouble is the last line of the body:

    return str(smallest) + ' ' + str(largest)

and what every caller then has to do before it can count anything:

report = divisor_report(84, 60)
space = report.find(' ')
low = int(report[:space])
high = int(report[space + 1:])
print('the gap is', high - low)
Why it fails

It runs, and the shape is still wrong. The caller gets characters, so it must know that a space separates the two numbers, find it, slice on either side and call int twice, and every caller repeats that. The value itself never says that there are two numbers in it; only the docstring does.

DefinitionDefinition 6.1: tuple
Conditions
  • Written with round brackets and commas: (1, 'two', 3). The commas are what build it; a one item tuple must be written (5,) with the trailing comma, and () is the empty tuple.

  • Ordered, so there is a first item and indexing works: t[0], t[-1], and slices t[1:3] which hand back a new tuple.

  • May mix types freely, including tuples and lists as items.

  • Immutable: no item can be replaced, and the tuple cannot grow or shrink. t[0] = 5 is a TypeError.

  • Supports + to join two tuples, * to repeat one, len for how many items, in to test membership, and for to walk the items.

  • Both + and * build a new tuple. t1 = t1 + t2 does not change the old tuple, it binds the name to a new one.

$$\boxed{\texttt{t = (v}_0\texttt{, v}_1\texttt{, ..., v}_{n-1}\texttt{)}\quad\text{indices }0\ \text{to}\ n-1,\ \text{no item may be replaced}}$$

A tuple is a row of values in a fixed order, reached by position, that cannot be edited after it is made. Anything that looks like editing one actually makes a second tuple.

Looks like this, but is not

One value in brackets should be a tuple of one item.

one = (5)
two = (5,)
print(one, type(one))
print(two, type(two))
print(len(two))

It prints 5 <class 'int'> and then (5,) <class 'tuple'>. Brackets around one value are the brackets of arithmetic, so (5) is the number 5 and has no length. The comma is what makes a tuple, which is why a tuple of one item looks unfinished and is written that way anyway.

Smallest and largest common divisor, handed back together

Write a function that takes two positive whole numbers and hands back both the smallest and the largest of their common divisors above 1, and (None, None) when they have none.

First, in full, the version that glues the two numbers into a string, so that the two can be compared line for line:

def divisor_report(n1, n2):
    """Assumes n1 and n2 are ints greater than 0.
    Returns a string holding the smallest and the largest common divisor.
    """
    smallest = 0
    largest = 0
    for d in range(2, min(n1, n2) + 1):
        if n1 % d == 0 and n2 % d == 0:
            if smallest == 0:
                smallest = d
            largest = d
    return str(smallest) + ' ' + str(largest)

report = divisor_report(84, 60)
print('the report says:', report)
space = report.find(' ')
low = int(report[:space])
high = int(report[space + 1:])
print('the gap between them is', high - low)

Sample Run:

the report says: 2 12
the gap between them is 10

And now the same job with a tuple. The loop is untouched; only the last line of the body and the three lines of the caller change.

def common_divisors(n1, n2):
    """Assumes n1 and n2 are ints greater than 0.
    Returns a tuple (smallest, largest) of the common divisors greater than 1.
    Returns (None, None) when there is no such divisor.
    """
    smallest = None
    largest = None
    for d in range(2, min(n1, n2) + 1):
        if n1 % d == 0 and n2 % d == 0:
            if smallest == None:
                smallest = d
            largest = d
    return (smallest, largest)

pair = common_divisors(84, 60)
print('for 84 and 60:', pair)
print('smallest is', pair[0], 'and largest is', pair[1])
print('the gap between them is', pair[1] - pair[0])
print('for 13 and 8:', common_divisors(13, 8))

Sample Run:

for 84 and 60: (2, 12)
smallest is 2 and largest is 12
the gap between them is 10
for 13 and 8: (None, None)
FindOne value that carries both answers, and the arithmetic the caller can then do with it.
Given
  • The two numbers are 84 and 60, and then 13 and 8.

  • A divisor counts only if it divides both numbers exactly, and 1 does not count.

Solution

Decide the shape of the answer before the loop

$$\text{answer} = (\text{smallest},\ \text{largest})$$

Two numbers with different meanings, always both present, order fixed by the docstring: that is a tuple rather than a list, because nothing about the answer will ever be appended to.

$$\texttt{smallest = None}$$

A starting value that cannot be mistaken for a real divisor, which 0 could be in other questions and None never can.

One pass finds both ends

$$\texttt{for d in range(2, min(n1, n2) + 1)}$$

No common divisor can exceed the smaller number, and starting at 2 is how 1 is excluded without a special case.

$$\texttt{if smallest == None: smallest = d}$$

The first divisor found is the smallest, because the loop goes upwards; the test is what stops the later ones overwriting it.

$$\texttt{largest = d}$$

Assigned every time, so when the loop ends it holds the last and therefore the biggest divisor.

Return the pair and use it

$$\texttt{return (smallest, largest)}$$

One value leaves the function, and it has two parts. The brackets here are the tuple, not the call.

$$\texttt{pair[1] - pair[0]} = 12 - 2 = 10$$

The caller indexes the answer instead of cutting up a string, and the result is a number it can do arithmetic with straight away.

Answer $$\boxed{(2,\ 12)\ \text{for}\ 84\ \text{and}\ 60;\quad (\texttt{None},\ \texttt{None})\ \text{for}\ 13\ \text{and}\ 8}$$
Check

Check the two ends by hand and from the other direction. 84 is 2 times 42 and 60 is 2 times 30, so 2 divides both and no smaller divisor above 1 exists.

One loop, two names, one tuple built at the end. The string version needed a loop, two names, two conversions to string, and then in every caller a find, two slices and two calls to int.

When a question says and, the answer is often a tuple. Deciding that before writing the loop is what keeps the body to one pass.

Checkpoint
§06.1 — joining, slicing and nesting in four lines

Thirty seconds. Three tuples, and the last one has a tuple inside it.

t = (7, 'lab', 2.5)
u = t[1:] + (7,)
print(u)
print(len(u), u[-1] + t[0])
print(t)
Find(a) Write the three lines this prints.
Given
  • t is (7, 'lab', 2.5).

  • u is built from a slice of t joined to a one item tuple.

IPython console
Hint 1/4

Work out what u is first and write it down, then answer the three prints from that. The last line is asking something about t, not about u.

Hint 2/4

A slice stops before its second index and hands back a new tuple. + joins two tuples into a third and changes neither.

Hint 3/4

Here t[1:] is everything from index 1 on, which is ('lab', 2.5), and the tuple joined to it is (7,).

Hint 4/4

u has three items, u[-1] + t[0] adds two numbers, and t is exactly what it was.

Show solution

Build u once and keep it

$$\texttt{t[1:]}\;\rightarrow\;\texttt{('lab', 2.5)}$$

From index 1 to the end, so the 7 is left out.

$$\texttt{+ (7,)}\;\rightarrow\;\texttt{('lab', 2.5, 7)}$$

The trailing comma makes the right hand side a tuple; without it this line would be a TypeError.

Answer the prints from it

$$\texttt{len(u)} = 3,\ \texttt{u[-1] + t[0]} = 7 + 7 = 14$$

Two numbers, so the plus is arithmetic here and not joining.

$$\texttt{t} = \texttt{(7, 'lab', 2.5)}$$

Nothing in the program could have changed it, because tuples have no operation that changes one.

Answer $$\boxed{\texttt{('lab', 2.5, 7)}\;/\;\texttt{3 14}\;/\;\texttt{(7, 'lab', 2.5)}}$$
Check

Independent check on the middle line: u[-1] is the last item of u, which the first printed line shows as 7, and t[0] is the first item of t, which the third printed line shows as 7.

⚠ Writing a one item tuple without the comma

Brackets look like what makes a tuple, and in every other line of Python they group an expression.

wrong$$\texttt{t = (5)}\;\Rightarrow\;\texttt{len(t)}\;\text{is a TypeError}$$
right$$\texttt{t = (5,)}\;\Rightarrow\;\texttt{len(t)} = 1$$
⚠ Expecting a join to change the tuple it started from

t1 = t1 + t2 reads like an instruction to grow t1, and with lists there is an operation that really does that.

wrong$$\texttt{t1 + t2}\;\Rightarrow\;\texttt{t1}\;\text{is longer}$$
right$$\texttt{t1 + t2}\;\Rightarrow\;\text{a new tuple};\ \texttt{t1}\;\text{unchanged}$$
⚠ Assigning to an item of a tuple

Indexing works for reading, so it looks as though it will work for writing, which is true of lists and of nothing else here.

wrong$$\texttt{week[0] = 'sun'}$$
right$$\texttt{week = ('sun',) + week[1:]}$$

6.2Ranges: a sequence that is worked out, not stored

A range is an immutable sequence of whole numbers that knows its three arguments and hands out values one at a time.

The tool that has driven every counted loop so far turns out to be one of these types too, and it has been a structured value all along.

DefinitionDefinition 6.2: range
Conditions
  • range(start, stop, step) stands for start, start plus step, start plus twice step, and so on, stopping before stop. With two arguments the step is 1; with one argument the start is 0 as well.

  • Immutable, like a tuple: no item can be replaced.

  • Indexing and slicing work. An index gives an int, a slice gives another range.

  • len, in and for all work. + and * do not: joining or repeating a range is a TypeError.

  • == on two ranges compares the sequences of integers they stand for, not the three numbers they were written with.

  • Printing a range shows the call rather than the values, so print(range(5)) puts range(0, 5) on the screen.

$$\boxed{\texttt{range(a, b, c)} \equiv a,\ a+c,\ a+2c,\ \dots\ \text{while still short of}\ b}$$

A range is a promise to count from the start towards the stop in steps of the given size, never reaching the stop, and it keeps the promise one value at a time instead of storing the values anywhere.

Looks like this, but is not

Two ranges with the same values should be equal.

a = range(0, 7, 2)
b = range(0, 8, 2)
c = range(6, -1, -2)
print(a == b)
print(a == c)
print(len(a), len(c))
first = ()
for value in c:
    first = first + (value,)
print(first)

It prints True, then False. Both ranges hold 0, 2, 4 and 6, and both have length 4, but one counts up and the other counts down. A sequence is its values in their order, so reversing them makes a different sequence.

Reading length, ends and membership off range(3, 30, 4)

Without listing the values by hand, say how many there are, what the first and last ones are, what a slice of the range is, and whether two given numbers are in it. Then add them up.

r = range(3, 30, 4)
print(r)
print(len(r), r[0], r[-1])
print(r[1:3])
print(19 in r, 20 in r)
total = 0
for value in r:
    total = total + value
print('total', total)

Sample Run:

range(3, 30, 4)
7 3 27
range(7, 15, 4)
True False
total 105
FindThe five printed facts, and where the last value comes from.
Given
  • The range is range(3, 30, 4).

  • Nothing is converted to a tuple or a list anywhere in the program.

Solution

Find the values without writing them all out

$$3,\ 7,\ 11,\ 15,\ 19,\ 23,\ 27$$

Start at 3 and add 4 while still under 30. The next one would be 31, which is over the stop, so it never appears.

$$\texttt{len(r)} = 7$$

Seven values, and the length is available without walking the range because a range knows its own arithmetic.

The two ends, and why the last is not 29

$$\texttt{r[0]} = 3,\ \texttt{r[-1]} = 27$$

Indexing a range gives an int. The last value is the largest one reachable by adding 4 to 3, not the largest number below the stop.

Membership and the slice

$$\texttt{19 in r}\;\rightarrow\;\texttt{True},\quad \texttt{20 in r}\;\rightarrow\;\texttt{False}$$

19 is 3 plus four steps; 20 is between two of the values, and being inside the interval is not the same as being in the sequence.

$$\texttt{r[1:3]}\;\rightarrow\;\texttt{range(7, 15, 4)}$$

Slicing hands back another range rather than a tuple, and it reports itself with its own start, stop and step, which is why the printed stop is 15 and not 11.

The total

$$3+7+11+15+19+23+27 = 105$$

The for loop walks the same seven values the arithmetic above predicted, which is what makes the total a check on them.

Answer $$\boxed{7\ \text{values},\ 3\ \text{to}\ 27,\ \texttt{r[1:3]} = \texttt{range(7, 15, 4)},\ \text{total}\ 105}$$
Check

Independent check on the total: the seven values are evenly spaced, so their average is the middle one, 15, and seven times 15 is 105.

The printed form of a slice of a range is the one thing here that surprises people in an exam.

Why print(range(10)) does not show any numbers

A range is asked to show itself, and then asked to show itself again after being turned into a tuple.

r = range(1, 6)
print(r)
print(tuple(r))
print(r[0] + r[4])

Sample Run:

range(1, 6)
(1, 2, 3, 4, 5)
6
FindWhy the first and second lines look nothing like each other.
Given
  • The range is range(1, 6), so the step is 1.

  • tuple(r) builds a tuple out of the values of r.

Solution

What a range keeps

$$\texttt{range(1, 6)}$$

The object holds the three numbers and nothing else. There is no row of items anywhere for print to show, so it shows the recipe.

$$\texttt{tuple(r)}\;\rightarrow\;\texttt{(1, 2, 3, 4, 5)}$$

Asking for a tuple makes the values exist all at once, which is the only point at which they are stored.

Indexing needs no conversion

$$\texttt{r[0] + r[4]} = 1 + 5 = 6$$

Each index is worked out by arithmetic on the start and the step, so a range can be indexed without ever being stored.

Answer $$\boxed{\texttt{range(1, 6)}\;/\;\texttt{(1, 2, 3, 4, 5)}\;/\;6}$$
Check

A check from the other end: len(range(1, 6)) is 5 and the tuple printed has five items, so nothing was lost in the conversion and nothing was invented by it.

This is why a debugging print on a range tells you so little, and why turning it into a tuple for one line is a reasonable thing to do while you are working.

Checkpoint
§06.2 — four facts about one range

Thirty seconds, and no listing of values on paper if you can avoid it.

r = range(2, 12, 3)
print(r[2], len(r))
print(11 in r, 12 in r)
print(r == range(2, 13, 3))
Find(a) Write the three lines this prints.
Given
  • The range is range(2, 12, 3).

  • The last line compares it with range(2, 13, 3).

IPython console
Hint 1/4

The last line is not asking whether the two ranges were written the same way. It is asking whether they hand out the same numbers in the same order.

Hint 2/4

A range stops before its stop value, and two ranges are equal when the sequences they stand for are equal.

Hint 3/4

From 2 in steps of 3 while under 12 gives 2, 5, 8, 11. While under 13, the next one after 11 would be 14, so nothing is added.

Hint 4/4

Four values, the third of them is at index 2, 11 is one of them and 12 is not, and the two ranges are equal.

Show solution

List the values once

$$2,\ 5,\ 8,\ 11$$

Adding 3 each time and stopping before 12.

$$\texttt{r[2]} = 8,\ \texttt{len(r)} = 4$$

Index 2 is the third value, since counting starts at 0.

The two comparisons

$$\texttt{11 in r}\;\rightarrow\;\texttt{True},\ \texttt{12 in r}\;\rightarrow\;\texttt{False}$$

12 is the stop, and a range never reaches its stop.

$$\texttt{range(2, 13, 3)} \equiv 2,\ 5,\ 8,\ 11$$

The step lands on 11 and the next would be 14, so raising the stop from 12 to 13 adds nothing and the two sequences are identical.

Answer $$\boxed{\texttt{8 4}\;/\;\texttt{True False}\;/\;\texttt{True}}$$
Check

Check the equality the long way: walk both ranges into tuples and they both come out as (2, 5, 8, 11). Same four numbers, same order, so True is right.

⚠ Expecting print to show the numbers of a range

Every other structured value prints its contents, so a range looks broken when it does not.

wrong$$\texttt{print(range(5))}\;\Rightarrow\;\texttt{[0, 1, 2, 3, 4]}$$
right$$\texttt{print(range(5))}\;\Rightarrow\;\texttt{range(0, 5)}$$
⚠ Joining or repeating ranges

Tuples and strings both allow + and *, and a range behaves like a tuple in every other respect.

wrong$$\texttt{range(3) + range(3, 6)}$$
right$$\texttt{tuple(range(3)) + tuple(range(3, 6))}$$
⚠ Reading the stop as the last value

The word stop sounds like where it ends up rather than where it gives up.

wrong$$\texttt{range(3, 30, 4)}\;\Rightarrow\;\text{last is}\ 30\ \text{or}\ 29$$
right$$\texttt{range(3, 30, 4)}\;\Rightarrow\;\text{last is}\ 27$$

6.3Lists, and the two families every list operation belongs to

A list is a sequence you may write on, so each of its operations either changes it and returns None or leaves it alone and returns something.

Tuples and ranges refuse to be edited, which is exactly what a growing collection of marks or words needs to be able to do.

RuleRule 6.3: the two families of list operation
Conditions
  • Written with square brackets: [2, 8, 3], and [] is the empty list. Ordered, indexable, sliceable, and it may hold values of any types including other lists.

  • , which is the whole difference: L[i] = value replaces an item, and the list can grow and shrink.

  • Family one changes the list and hands back None: append(e) adds one item at the end, extend(L2) adds all the items of another list, insert(i, e) puts one item at a position, remove(e) deletes the first item equal to e, reverse() turns it round, sort() puts it in order.

  • Family two leaves the list alone and hands back a value: sorted(L) a new list in order, L + L2 a new joined list, L[a:b] a new list, list(L) a new list, len(L) an int, L.index(e) an int, L.count(e) an int, sum(L), max(L), min(L) numbers.

  • L.pop(i) is in both families at once: it removes the item at i, changes the list, and hands the removed item back.

  • remove(e) with no such item is a and index(e) with no such item is a ValueError, so test with in first.

$$\boxed{\text{changes }L\Rightarrow\texttt{None}\ \text{back};\quad \text{hands a value back}\Rightarrow L\ \text{untouched};\quad \texttt{pop}\ \text{does both}}$$

For every list operation ask two questions, not one: did it edit the list I gave it, and what did it hand me back. Almost every operation answers yes to one and no to the other, and only pop answers yes to both.

Looks like this, but is not

sorted and sort differ by two letters, so best = L.sort() should keep a sorted list in best.

marks = [70, 45, 90]
best = marks.sort()
print(best)
print(marks)

It prints None, then the sorted list. The sorting really happened, in marks itself, and best holds the None that sort handed back. The version that fills a name is best = sorted(marks), and that one leaves marks in its original order.

Six list methods in a row, with the list printed after each

Run the six operations of the first family one after another on the same list and print it each time, so that the shape of each change is visible rather than remembered.

marks = [55, 90, 70]
print(marks, len(marks))
marks.append(48)
print('after append  ', marks)
marks.insert(1, 100)
print('after insert  ', marks)
gone = marks.pop(2)
print('pop gave back ', gone, 'and left', marks)
marks.remove(48)
print('after remove  ', marks)
marks.reverse()
print('after reverse ', marks)

Sample Run:

[55, 90, 70] 3
after append   [55, 90, 70, 48]
after insert   [55, 100, 90, 70, 48]
pop gave back  90 and left [55, 100, 70, 48]
after remove   [55, 100, 70]
after reverse  [70, 100, 55]
FindWhat each of the six lines leaves behind, and which one also hands something back.
Given
  • The list starts as [55, 90, 70].

  • Each line after the first changes the same list.

Solution

Adding at the end and in the middle

$$\texttt{append(48)}\;\rightarrow\;\texttt{[55, 90, 70, 48]}$$

One item at the end, always. Appending a list would add that list as a single item, which is what extend exists to avoid.

$$\texttt{insert(1, 100)}\;\rightarrow\;\texttt{[55, 100, 90, 70, 48]}$$

The new item takes position 1 and everything from there on moves one place right.

Taking away by position and by value

$$\texttt{pop(2)}\;\rightarrow\;90\;\text{returned}$$

Position 2 held 90 after the insert, so the value that comes back depends on the previous line; this is why pop is easy to get wrong in a trace.

$$\texttt{remove(48)}\;\rightarrow\;\texttt{[55, 100, 70]}$$

By value, not by position, and only the first match goes. It hands back nothing, so there is no way to see what was removed.

Turning it round

$$\texttt{reverse()}\;\rightarrow\;\texttt{[70, 100, 55]}$$

In place and returning None, like the rest of this family. The version that hands a value back is a slice with a negative step, which this course does not use.

Answer $$\boxed{\texttt{[55, 90, 70]}\to\texttt{[55, 100, 90, 70, 48]}\to\texttt{[70, 100, 55]}}$$
Check

Count the items at each step as a check on the story: 3, then 4 after the append, 5 after the insert, 4 after the pop, 3 after the remove, 3 after the reverse.

Six lines, six changes to one object, no new list built anywhere. Doing the same thing with only the second family would build six new lists and need a name for each.

In a trace, write the list out again after every line of this family. The value pop hands back depends on what the line before it did.

extend against append against plus, on the same two lists

Three operations that all put two lists together, and they leave three different things behind. This is the trace the lecture spends the most time on.

L1 = [1, 2, 3]
L2 = [4, 5]
L3 = L1 + L2
print('L3 =', L3)
print('L1 =', L1)
L1.extend(L2)
print('after extend, L1 =', L1)
L1.append(L2)
print('after append, L1 =', L1)
print('length of L1 =', len(L1))
print('last item    =', L1[-1])

Sample Run:

L3 = [1, 2, 3, 4, 5]
L1 = [1, 2, 3]
after extend, L1 = [1, 2, 3, 4, 5]
after append, L1 = [1, 2, 3, 4, 5, [4, 5]]
length of L1 = 6
last item    = [4, 5]
FindWhat each of the three joins does to L1, and why the last item ends up being a list.
Given
  • L1 is [1, 2, 3] and L2 is [4, 5].

  • L3 is built with the plus operator before anything else happens.

Solution

Plus builds and changes nothing

$$\texttt{L3 = L1 + L2}\;\rightarrow\;\texttt{[1, 2, 3, 4, 5]}$$

A third list is built out of the items of both, and the printed L1 on the next line proves it was not touched.

extend pours the items in

$$\texttt{L1.extend(L2)}\;\rightarrow\;\texttt{[1, 2, 3, 4, 5]}$$

The items of L2 are added one by one to L1 itself. L1 now looks like L3 but is a different object, and L2 is unchanged.

append adds one item, whatever it is

$$\texttt{L1.append(L2)}\;\rightarrow\;\texttt{[1, 2, 3, 4, 5, [4, 5]]}$$

One item goes on the end, and that item is the list L2 itself. The brackets in the printed line are how a list inside a list looks.

$$\texttt{len(L1)} = 6,\ \texttt{L1[-1]} = \texttt{[4, 5]}$$

Six items, not seven: the nested list counts as one. This is the line that settles the difference between the two methods.

Answer $$\boxed{\texttt{+}\ \text{builds},\ \texttt{extend}\ \text{adds items},\ \texttt{append}\ \text{adds one item}}$$
Check

An independent check on the last item: len(L1[-1]) would be 2, because that item is a list of two numbers, whereas len(L1[0]) would be an error because 1 is a number and has no length.

If an exam line has append and the argument is a list, expect the answer to have brackets inside brackets and count the items carefully.

Checkpoint
§06.3 — two families in two lines

Thirty seconds. One line uses an operation from each family, the other uses one from the first family in a place it does not belong.

L = [4, 1, 3]
print(L.count(3), sorted(L))
print(L.sort(), L)
Find(a) Write the two lines this prints.
Given
  • L is [4, 1, 3].

  • print works out its arguments from left to right before printing anything.

IPython console
Hint 1/4

On the second line, two things are printed and one of them is a call. Ask what that call hands back, and separately what it does to the list before the second argument is looked at.

Hint 2/4

sorted(L) hands back a new list and leaves L alone. L.sort() sorts L itself and hands back None. Arguments are worked out left to right.

Hint 3/4

Here the first line calls L.count(3) and sorted(L) on [4, 1, 3], and the second line calls L.sort() and then reads L.

Hint 4/4

The first line ends with a sorted new list while L is still unsorted; on the second line the word None is followed by an L that is now in order.

Show solution

First line, two calls from two families

$$\texttt{L.count(3)} = 1$$

Counting hands back an int and changes nothing, so L is still [4, 1, 3] after it.

$$\texttt{sorted(L)}\;\rightarrow\;\texttt{[1, 3, 4]}$$

A new list. L is untouched, which is why the sorting on the next line still has work to do.

Second line, in the order the interpreter reads it

$$\texttt{L.sort()}\;\rightarrow\;\texttt{None}$$

First argument worked out first: the list is rewritten here, and the value to be printed is None.

$$\texttt{L}\;\rightarrow\;\texttt{[1, 3, 4]}$$

Second argument read after the sort has already happened, so the printed list is the sorted one.

Answer $$\boxed{\texttt{1 [1, 3, 4]}\;/\;\texttt{None [1, 3, 4]}}$$
Check

Check the claim that sorted left L alone: if it had sorted L, the None line would still print [1, 3, 4] and nothing would distinguish the two.

⚠ Assigning from an operation of the first family

Every other language has a sort that hands the list back, and the line reads like English.

wrong$$\texttt{best = marks.sort()}$$
right$$\texttt{best = sorted(marks)}\quad\text{or}\quad\texttt{marks.sort()}$$
⚠ Using append where extend was meant

Both add things at the end, and with a single number as the argument they even look the same.

wrong$$\texttt{L1.append(L2)}\;\Rightarrow\;\texttt{[1, 2, 3, [4, 5]]}$$
right$$\texttt{L1.extend(L2)}\;\Rightarrow\;\texttt{[1, 2, 3, 4, 5]}$$
⚠ Calling index or remove without checking first

It works on every test where the item is present, which is every test you write while the code is fresh.

wrong$$\texttt{friends.index('Zeynep')}\;\Rightarrow\;\text{ValueError}$$
right$$\texttt{if 'Zeynep' in friends: friends.index('Zeynep')}$$
⚠ Reading pop as remove by value

The two words are close in meaning and both take one argument.

wrong$$\texttt{L.pop(48)}\;\Rightarrow\;\text{removes the item}\ 48$$
right$$\texttt{L.pop(48)}\;\Rightarrow\;\text{item at index}\ 48;\ \texttt{L.remove(48)}\ \text{by value}$$

6.4Two names, one list: aliasing, cloning and side effects

An assignment copies a reference and not an object, so a change made through one name is visible through every other name for it.

Everything in the previous block changed a list in place, and that raises a question the earlier sections never had to ask: whose list was it.

RuleRule 6.4: what a name holds
Conditions
  • A name never holds a structured object. It holds a reference to one, so b = a makes a second name for the same object and copies nothing.

  • Two or more names for one object are . Changing the object through one of them changes it for all of them, because there is only one object.

  • a is b is True only when the two names refer to one object. a == b is True when the contents match, whether or not they are the same object.

  • Cloning a list takes a[:] or list(a). Both build a new list with the same items, so the two lists can then be changed independently.

  • A clone is one level deep. The new list holds the same item objects, so if an item is itself a list, both lists share it.

  • Passing a list to a function passes the reference, so the body can change the caller's list. Rebinding the parameter with values = ... does not, because that only points the local name somewhere else.

  • None of this is true of ints, floats, strings, tuples or ranges. There is no operation that changes one of those, so an alias for one can never surprise you.

$$\boxed{\texttt{b = a}\Rightarrow\ \text{one object};\quad \texttt{b = a[:]}\Rightarrow\ \text{two objects};\quad \texttt{is}\ \text{asks which},\ \texttt{==}\ \text{asks what}}$$

Assignment hands over the address of the object rather than a copy of it, so after it there are two ways in to one thing. A slice of the whole list is the cheap way to get a second thing.

Looks like this, but is not

A full slice clones a list, so it should protect everything inside it.

rows = [[1, 2], [3, 4]]
copy = rows[:]
print('copy is rows :', copy is rows)
copy.append([5, 6])
print('rows after append :', rows)
copy[0].append(99)
print('rows after inner  :', rows)

The clone is a different object, which the False on the first line shows, and appending a new row to it leaves rows alone. The third line is the catch: copy[0] and rows[0] are the same inner list, so appending 99 to it shows up in both.

The backup that sorted itself, and the one line that fixes it

This is the program from the top of the page. It saves the marks under a second name before sorting, prints both, and the backup comes out sorted too.

marks = [72, 45, 91, 60]
backup = marks
marks.sort()
print('sorted :', marks)
print('backup :', backup)

Sample Run:

sorted : [45, 60, 72, 91]
backup : [45, 60, 72, 91]

Now the same program with the second line changed, and a second way of writing the same fix.

marks = [72, 45, 91, 60]
backup = marks[:]
other = list(marks)
marks.sort()
print('sorted    :', marks)
print('backup    :', backup)
print('other copy:', other)

Sample Run:

sorted    : [45, 60, 72, 91]
backup    : [72, 45, 91, 60]
other copy: [72, 45, 91, 60]
FindWhy the first backup changed when nobody wrote to it, and what the fix costs.
Given
  • The marks are [72, 45, 91, 60].

  • Only the line that makes the backup differs between the two programs.

Solution

Read the assignment as what it is

$$\texttt{backup = marks}$$

No new list is built here. The right hand side is a reference and that reference is what gets copied, so after this line there is one list with two names.

$$\texttt{marks.sort()}$$

An operation of the first family: it rewrites the object rather than building a new one, and the object is the one both names point at.

Why the printed lines are identical

$$\texttt{print(backup)}\;\rightarrow\;\texttt{[45, 60, 72, 91]}$$

There is nothing else it could print. The name reaches the same slots the sort just rearranged.

The fix, and what it does differently

$$\texttt{backup = marks[:]}$$

Slicing is in the second family, so it builds a new list. The two names now reach two different rows of slots and the sort can only reach one of them.

$$\texttt{other = list(marks)}$$

The same thing said the other way. Both appear in the lecture; neither is deeper than the other.

Answer $$\boxed{\texttt{backup = marks}\Rightarrow\text{both sorted};\quad \texttt{backup = marks[:]}\Rightarrow\text{original kept}}$$
Check

An independent check that does not involve sorting at all: add print(backup is marks) to each program. The first prints True and the second prints False, which is the whole difference, visible before anything is changed.

The fix is four characters and one new list of four items. The bug it prevents is silent: the first program runs, prints two lines and never reports an error.

Whenever a program keeps a before and an after, look at the line that made the before. If it has no slice and no call to list in it, there is no before.

A tuple that cannot be changed, holding a list that can

The trap that a past midterm paper used, with different data. A tuple holds a string, a list and a number. Three lines succeed, the fourth stops the program.

record = ('cs115', [55, 90], 2024)
print(record)
record[1].append(70)
print(record)
part = record[1][0:1]
part = part + part
print(part, type(part))
record[1] = [0]
print('this line never runs')

Sample Run:

('cs115', [55, 90], 2024)
('cs115', [55, 90, 70], 2024)
[55, 55] <class 'list'>
Traceback (most recent call last):
  File "record.py", line 8, in <module>
    record[1] = [0]
    ~~~~~~^^^
TypeError: 'tuple' object does not support item assignment
FindWhich changes go through, which does not, and what the type of part is.
Given
  • The tuple is ('cs115', [55, 90], 2024).

  • part is built from a slice of the list inside the tuple.

Solution

The slot cannot be replaced, its contents can

$$\texttt{record[1].append(70)}\;\rightarrow\;\text{works}$$

The tuple is not being changed at all here. It still holds the same three references; the second of them points at a list, and it is that list that grew.

$$\texttt{record[1] = [0]}\;\rightarrow\;\text{TypeError}$$

This one really does ask the tuple to hold a different reference in its second slot, and that is the thing a tuple refuses to do.

The slice, and the type it hands back

$$\texttt{record[1][0:1]}\;\rightarrow\;\texttt{[55]}$$

Slicing a list gives a list, even when the slice has one item. It is also a new list, so what happens to it next cannot reach the tuple.

$$\texttt{part = part + part}\;\rightarrow\;\texttt{[55, 55]}$$

Joining two lists builds a third and rebinds the local name. The list inside the tuple is not involved, which is why the printed type is list and the record still shows the appended 70.

Answer $$\boxed{\texttt{('cs115', [55, 90, 70], 2024)},\ \texttt{[55, 55]},\ \text{then TypeError}}$$
Check

A check that separates the two claims: print(len(record)) is 3 before and after the append, so the tuple genuinely did not change, while print(len(record[1])) goes from 2 to 3.

Immutable is a promise about the slots. Read a tuple in an exam question by asking what is in each slot, and then whether that thing has any operation that changes it.

A default value that remembers the last call

A function collects values into a list, and the list is the default value of its second parameter. Called three times with one argument, it does not start empty.

def collect(value, seen=[]):
    """Assumes value is a number. Adds value to seen and returns seen."""
    seen.append(value)
    return seen

print(collect(1))
print(collect(2))
print(collect(3))

def collect_safe(value, seen=None):
    """Assumes value is a number.
    Returns a list holding value, added to seen when seen was given.
    """
    if seen == None:
        seen = []
    seen.append(value)
    return seen

print(collect_safe(1))
print(collect_safe(2))

Sample Run:

[1]
[1, 2]
[1, 2, 3]
[1]
[2]
FindWhy the second call prints two items, and what the safe version does differently.
Given
  • The header is def collect(value, seen=[]).

  • The three calls all pass one argument, so the default is used every time.

Solution

When the default is worked out

$$\texttt{seen=[]}\;\text{runs once}$$

The default is worked out when the def line is read, not on each call, so one list is made and every call that uses the default gets that same list.

$$\texttt{seen.append(value)}$$

An operation of the first family on an object that outlives the call, so the next call sees what the previous one left.

The safe shape

$$\texttt{seen=None}$$

None is immutable, so there is nothing for a call to change. It is a flag rather than a value.

$$\texttt{if seen == None: seen = []}$$

The new list is built inside the body, which means once per call, which is what the docstring was promising all along.

Answer $$\boxed{\texttt{[1]},\ \texttt{[1, 2]},\ \texttt{[1, 2, 3]}\quad\text{then}\quad\texttt{[1]},\ \texttt{[2]}}$$
Check

An independent check on the diagnosis: print(collect(4) is collect(5)) would print True, because both calls hand back the one list that was built when the def was read.

A default value that can be changed is the one place where two separate calls can quietly share a structure.

Checkpoint
§06.4 — one assignment, one slice, two appends

Thirty seconds. Two names are made from one list, in the two different ways.

a = [1, 2]
b = a
c = a[:]
b.append(3)
c.append(4)
print(a, b, c)
print(a is b, a is c, a == b)
Find(a) Write the two lines this prints.
Given
  • a starts as [1, 2].

  • b is made by assignment and c by a full slice.

IPython console
Hint 1/4

Draw the names and the objects before you read the appends. The question is how many list objects exist after the first three lines.

Hint 2/4

Assignment makes a second name for one object. A full slice builds a second object. A change through a name reaches whatever that name points at.

Hint 3/4

Here b = a makes an alias and c = a[:] makes a clone, so the 3 appended through b lands in the object a also names, and the 4 appended through c does not.

Hint 4/4

Two of the three printed lists are identical and the third has the 4 in it; on the second line, one comparison is False.

Show solution

Count the objects first

$$\texttt{b = a}\Rightarrow\ \text{one object, two names}$$

Nothing on that line builds a list.

$$\texttt{c = a[:]}\Rightarrow\ \text{a second object}$$

Slicing is the operation that builds, so now there are two.

Send each append to its object

$$\texttt{b.append(3)}\Rightarrow\ \texttt{a} = \texttt{[1, 2, 3]}$$

The object b names is the object a names.

$$\texttt{c.append(4)}\Rightarrow\ \texttt{c} = \texttt{[1, 2, 4]}$$

The clone was taken before the 3 was appended, so it still had two items when the 4 arrived.

Answer $$\boxed{\texttt{[1, 2, 3] [1, 2, 3] [1, 2, 4]}\;/\;\texttt{True False True}}$$
Check

Check the order of events rather than the values: the clone was made on line 3 and the 3 was appended on line 4, so the clone could not contain it.

⚠ Making a backup with a plain assignment

It is what a backup looks like, and for numbers and strings it even works, because nothing can change those.

wrong$$\texttt{backup = marks}$$
right$$\texttt{backup = marks[:]}\quad\text{or}\quad\texttt{list(marks)}$$
⚠ Expecting a clone to protect the lists inside it

The word copy sounds total, and for a list of numbers it is.

wrong$$\texttt{copy = rows[:]}\;\Rightarrow\;\texttt{copy[0]}\ \text{is safe}$$
right$$\texttt{copy[0]}\;\text{is}\;\texttt{rows[0]};\ \text{clone each row to separate them}$$
⚠ Using is to compare contents

It reads like the English word, and in a REPL two small equal ints can even give True.

wrong$$\texttt{if marks is [45, 60]:}$$
right$$\texttt{if marks == [45, 60]:}$$
⚠ Believing a tuple protects the list inside it

Immutable is taught as a property of the whole value rather than of its slots.

wrong$$\texttt{record[1].append(70)}\;\Rightarrow\;\text{TypeError}$$
right$$\texttt{record[1].append(70)}\;\Rightarrow\;\text{the inner list grows}$$
⚠ Rebinding a parameter and expecting the caller to see it

Changing an item through a parameter does reach the caller, so changing the parameter itself looks as though it should too.

wrong$$\texttt{values = [0, 0]}\;\text{inside a body}$$
right$$\texttt{values[0] = 0}\;\text{and}\;\texttt{values[1] = 0}$$
⚠ Giving a parameter an empty list as its default

It looks like the tidiest possible default and the bug only shows on the second call.

wrong$$\texttt{def collect(value, seen=[]):}$$
right$$\texttt{def collect(value, seen=None):}\;\text{then build it in the body}$$

6.5Dictionaries: looking a value up by a key instead of a position

A dictionary stores pairs and finds a value by its key, so data with names in it stops needing two parallel lists.

Positions are the wrong handle for most real data: nobody knows which index a student number sits at, and the list has to be searched to find out.

DefinitionDefinition 6.5: dictionary
Conditions
  • Written with curly braces and colons: {'Ada': 91, 'Bora': 68}, and {} is the empty dictionary.

  • d[key] hands back the value stored under that key. d[key] = value stores one, replacing what was there if the key already existed and adding a new pair if it did not.

  • Keys are unique and must be immutable. A string, a number or a tuple can be a key; a list or a dictionary cannot, and trying gives a TypeError about an .

  • Values can be anything at all, including lists, tuples and other dictionaries, and they may repeat.

  • A key that is not there is a KeyError. key in d tests for one, and d.get(key, fallback) hands back the fallback instead of stopping the program.

  • len(d) is the number of pairs. d.pop(key) removes a pair and hands the value back. d.keys() and d.values() give you can walk or pass to sum, and they are not lists, so they cannot be indexed.

  • for k in d: walks the keys, one at a time, and the value is then d[k]. There is no indexing by position anywhere: d[0] looks for the key 0.

$$\boxed{\texttt{d[k]}\ \text{finds the value stored under}\ k;\quad \text{keys unique and immutable, values anything}}$$

A dictionary is a set of labelled boxes. You fetch a box by saying its label, never by saying how many boxes along it is, and asking for a label that was never used stops the program unless you check first.

Looks like this, but is not

The brackets are the same brackets as on a list, so d[0] should be the first pair.

phone = {'Evren': 7445167, 'Ana': 6413354}
print(phone.get('Deniz', 'no number'))
print(phone[0])

The first line works and prints no number, because get was asked for a missing key with a fallback. The second line stops the program with KeyError: 0. A dictionary has no first pair to ask for; the 0 in the brackets is a key that was never stored, and if some pair happened to use 0 as its key you would silently get that pair instead of the first one.

How many times each character occurs in a phrase

Write a function that takes a string and hands back a dictionary from each character to how many times it occurs, then print the pairs in alphabetical order of the character.

def letter_counts(text):
    """Assumes text is a string.
    Returns a dictionary whose keys are the characters of text and whose
    values are how many times each character occurs.
    """
    counts = {}
    for ch in text:
        if ch in counts:
            counts[ch] = counts[ch] + 1
        else:
            counts[ch] = 1
    return counts

phrase = 'lab data'
table = letter_counts(phrase)
print(table)
for ch in sorted(table.keys()):
    print(repr(ch), table[ch])

Sample Run:

{'l': 1, 'a': 3, 'b': 1, ' ': 1, 'd': 1, 't': 1}
' ' 1
'a' 3
'b' 1
'd' 1
'l' 1
't' 1
FindThe dictionary, and why the printed order differs between the two halves of the output.
Given
  • The phrase is 'lab data', which has a space in it.

  • repr is used in the printing so that the space can be seen as a character.

Solution

The counting pattern, which is the same every time

$$\texttt{counts = \{\}}$$

Start empty, because nothing is known about which characters will turn up.

$$\texttt{if ch in counts: counts[ch] = counts[ch] + 1}$$

The key has to exist before it can be read, so the test comes first.

$$\texttt{else: counts[ch] = 1}$$

First sighting, so the pair is created with the value 1 rather than 0, which is the off by one this pattern invites.

Walking a string gives characters

$$\texttt{for ch in text}$$

No index is needed because nothing here is about position, and the space is a character like any other, which is why it gets a pair of its own.

Two different orders in one output

$$\text{first line: l, a, b, space, d, t}$$

The dictionary prints in the order the keys were first added, which is the order the characters first appear in the phrase.

$$\texttt{for ch in sorted(table.keys())}$$

Sorting the keys gives a list that can be walked in a fixed order, with the space first because its character code is below the letters.

Answer $$\boxed{\texttt{a}\mapsto 3,\ \texttt{l},\texttt{b},\texttt{d},\texttt{t},\ \text{space}\mapsto 1\ \text{each}}$$
Check

Independent check on the total: the counts are 3 for a and 1 for each of the five others, which adds to 8, and len('lab data') is 8.

Any question of the form how many of each is this dictionary and this three line pattern. The only decision left is what the key should be.

Student records: a dictionary of lists of tuples

All four structures at once, which is what a lab question about grades actually needs. The key is a student number, the value is a list, and each item of the list is a course and a mark held together.

records = {}
records['1234'] = [('econ101', 98.0)]
records['1234'].append(('cs115', 95.0))
records['9876'] = [('cs115', 80.0)]
print(records)
print(records['1234'][1])
print(records['1234'][1][0])
total = 0
for course in records['1234']:
    total = total + course[1]
print('average', total / len(records['1234']))

Sample Run:

{'1234': [('econ101', 98.0), ('cs115', 95.0)], '9876': [('cs115', 80.0)]}
('cs115', 95.0)
cs115
average 96.5
FindHow to reach a single mark through all three layers, and the average for one student.
Given
  • The keys are the strings '1234' and '9876'.

  • Each item of a value is a tuple of a course name and a float.

Solution

Build the layers in order

$$\texttt{records['1234'] = [('econ101', 98.0)]}$$

A new key with a list of one tuple. The brackets and the round brackets are both needed and they mean different things.

$$\texttt{records['1234'].append(('cs115', 95.0))}$$

The value is a list, so a second course is appended to it. The dictionary itself is not changed by this line at all.

Read through the layers

$$\texttt{records['1234'][1]}\;\rightarrow\;\texttt{('cs115', 95.0)}$$

Key first, then position in the list. The order of the brackets is the order of the layers.

$$\texttt{records['1234'][1][0]}\;\rightarrow\;\texttt{cs115}$$

A third bracket picks the first item out of the tuple, and it prints without quotes because print shows a string plainly.

Walk the list of tuples

$$\texttt{for course in records['1234']}$$

The loop variable is a tuple, so course[1] is the mark. Nothing needs unpacking and nothing needs an index over the list.

$$(98.0 + 95.0)/2 = 96.5$$

Two courses, so the length of the list is the divisor. Using a hard 2 here would break as soon as a third course was appended.

Answer $$\boxed{\texttt{('cs115', 95.0)},\ \texttt{cs115},\ \text{average}\ 96.5}$$
Check

Independent check on the average: 98 and 95 differ by 3, so their average is 3 halves below 98, that is 96.5. The printed value agrees without repeating the loop.

Write down the shape before the code: key is a student number, value is a list, item is a pair.

Checkpoint
§06.5 — one dictionary, six lines, two removals

Thirty seconds. Watch the third line, which reads a value and writes it back.

d = {'x': 1}
d['y'] = 2
d['x'] = d['x'] + 10
print(d, len(d))
print(d.pop('x'), d)
print('x' in d, d.get('x', 0))
Find(a) Write the three lines this prints.
Given
  • The dictionary starts as {'x': 1}.

  • get is called with a fallback of 0 at the end.

IPython console
Hint 1/4

Keep a running picture of the pairs, and remember that the last line is asked after something has been removed.

Hint 2/4

Assigning to an existing key replaces its value. pop removes the pair and hands the value back. get with a fallback never stops the program.

Hint 3/4

Here d['x'] = d['x'] + 10 turns the 1 into 11, and d.pop('x') then takes that pair away, leaving only the pair for y.

Hint 4/4

Two pairs, then the popped value with the one remaining pair, then a False and a 0.

Show solution

Build up the pairs

$$\texttt{d['y'] = 2}\Rightarrow\ \texttt{\{'x': 1, 'y': 2\}}$$

New key, so a pair is added at the end of the insertion order.

$$\texttt{d['x'] = 1 + 10}\Rightarrow\ \texttt{\{'x': 11, 'y': 2\}}$$

Existing key, so the value is replaced and the position in the order stays where it was, which is why x still prints first.

Remove and then ask

$$\texttt{d.pop('x')}\;\rightarrow\;11$$

The value comes back, not the key, and the pair is gone from the dictionary printed beside it.

$$\texttt{'x' in d}\;\rightarrow\;\texttt{False},\ \texttt{d.get('x', 0)}\;\rightarrow\;0$$

Both are safe questions about a missing key, which is the whole reason they exist.

Answer $$\boxed{\texttt{\{'x': 11, 'y': 2\} 2}\;/\;\texttt{11 \{'y': 2\}}\;/\;\texttt{False 0}}$$
Check

Check the length against the story: two assignments to a one pair dictionary gave 2 rather than 3, so one of them replaced; and after one pop the printed dictionary has one pair.

⚠ Indexing a dictionary by position

The notation is the same as a list and the pairs are printed in a visible order, so the first one looks reachable.

wrong$$\texttt{phone[0]}\;\Rightarrow\;\text{the first pair}$$
right$$\texttt{phone[0]}\;\Rightarrow\;\text{KeyError: 0}$$
⚠ Reading a key before it exists

The counting pattern is written from the middle outwards, and the line that adds one is the interesting one.

wrong$$\texttt{counts[ch] = counts[ch] + 1}\;\text{on a new key}$$
right$$\texttt{if ch in counts: ... else: counts[ch] = 1}$$
⚠ Using a list as a key

A list of two things is the natural way to write a compound label, and the error message names a word the course never used.

wrong$$\texttt{scores[['a', 'b']] = 1}\;\Rightarrow\;\text{TypeError: unhashable}$$
right$$\texttt{scores[('a', 'b')] = 1}$$
⚠ Overwriting a list valued entry instead of appending to it

The line that creates the first entry is written first and then copied for the second case.

wrong$$\texttt{groups[letter] = [name]}\;\text{every time}$$
right$$\texttt{groups[letter].append(name)}\;\text{when the key is there}$$
⚠ Treating keys() as a list

It walks like a list and prints something that looks close to one.

wrong$$\texttt{d.keys()[0]}$$
right$$\texttt{sorted(d.keys())[0]}\quad\text{or}\quad\texttt{list(d.keys())[0]}$$

6.6Functions are values too: passing one, storing one, defaulting to one

A function name without brackets is an ordinary value, so it can be passed to another function that then decides when to call it.

Three structures can now hold anything, and it turns out that anything includes the functions written in the previous sections.

RuleRule 6.6: a function name is a value, the brackets are the call
Conditions
  • f is the function object itself and can be assigned, stored in a list, a tuple or a dictionary, passed as an argument and handed back from a return.

  • f(x) is a call, and the whole expression stands for what the call handed back.

  • A function that takes a function as a parameter, or hands one back, is called a higher order function. The parameter is an ordinary parameter and gets an ordinary local name.

  • A parameter may have a function as its default value: def convert(value, using=int). A caller who passes nothing gets int, and a caller who passes abs gets abs.

  • The built in functions of the course, int, float, str, abs, bool, len, round, sorted, are values in exactly the same way and are the ones the lecture passes around.

  • Passing f() where f was meant calls it too early. What arrives is the result, and using that result as a function gives a TypeError saying the object is not callable.

$$\boxed{\texttt{f}\ \text{is the function};\quad \texttt{f(x)}\ \text{is its result};\quad \text{a parameter can hold either}}$$

Write the name on its own to hand somebody the tool; write the name with brackets to use the tool yourself. A higher order function is one that takes the tool and decides how many times to use it.

Looks like this, but is not

A function name is being used, so a pair of brackets after it cannot hurt.

def square(n):
    """Assumes n is a number. Returns n times n."""
    return n * n

def use_twice(f, value):
    """Assumes f is a function of one argument. Returns f of f of value."""
    return f(f(value))

print(use_twice(square, 3))
print(use_twice(square(3), 2))

The first call prints 81, since square arrives as a value and is used twice on 3. The second line passes square(3), which is the number 9, and the body then tries 9(9(2)).

One function that applies any other function to every item

The pattern the lecture builds this idea on. Write a function that takes a list and a function, and replaces every item by what the function makes of it. Then call it three times with three different functions, one of them your own.

def apply_to_each(values, f):
    """Assumes values is a list and f is a function of one argument.
    Replaces every item of values with f of that item and returns nothing.
    """
    for i in range(len(values)):
        values[i] = f(values[i])

def factorial(n):
    """Assumes n is an int greater than or equal to 0. Returns n factorial."""
    product = 1
    for i in range(1, n + 1):
        product = product * i
    return product

L = [1, -2, 3.33]
print('L =', L)
apply_to_each(L, abs)
print('after abs :', L)
apply_to_each(L, int)
print('after int :', L)
apply_to_each(L, factorial)
print('after fact:', L)

Sample Run:

L = [1, -2, 3.33]
after abs : [1, 2, 3.33]
after int : [1, 2, 3]
after fact: [1, 2, 6]
FindThe list after each call, and why the last one gives 6.
Given
  • The list starts as [1, -2, 3.33].

  • The three functions applied in turn are abs, int and a factorial written on the spot.

Solution

The parameter that holds a function

$$\texttt{def apply\_to\_each(values, f):}$$

Two ordinary parameters. Nothing in the header says that the second one is a function; the body says it by putting brackets after it.

$$\texttt{values[i] = f(values[i])}$$

Assigning to an item, so this changes the caller's list. Walking by index is what makes that possible, since walking the items would only rebind a local name.

Three calls, three different tools

$$\texttt{abs}\;\rightarrow\;\texttt{[1, 2, 3.33]}$$

Only the negative item moves, and the float keeps its decimals because abs does not round.

$$\texttt{int}\;\rightarrow\;\texttt{[1, 2, 3]}$$

Now the 3.33 becomes 3 by throwing the fraction away, which matters for the next line: the factorial needs whole numbers.

$$\texttt{factorial}\;\rightarrow\;\texttt{[1, 2, 6]}$$

One factorial is 1, two factorial is 2, three factorial is 6. A function written in this file is passed exactly as the built in ones were.

Answer $$\boxed{\texttt{[1, 2, 3.33]}\to\texttt{[1, 2, 3]}\to\texttt{[1, 2, 6]}}$$
Check

An independent check on the last step: the third call is only legal because the second one made every item a whole number. Swap the order of the int and factorial calls and the program stops, since a factorial loop over a range needs an int.

One function with a loop in it, used three times. Writing three functions with the same loop in each would be the same work three times and three places to fix a mistake.

This is the shape to recognise in an exam: a parameter that is used with brackets somewhere in the body is a function, and the question is what the caller handed over.

A conversion function chosen per item, with int as the default

A past paper used a header of this shape, with a function as the default value of the second parameter, and asked what the program printed. Here is the same idea on different data. Three branches choose three different tools for three kinds of reading.

def convert(value, using=int):
    """Assumes value is a number and using is a function of one argument.
    Returns using applied to value.
    """
    return using(value)

readings = [-40.5, 0, 12.25, -3.75, 91.6]
for i in range(len(readings)):
    if readings[i] > 50:
        readings[i] = convert(readings[i])
    elif readings[i] > -10:
        readings[i] = convert(readings[i], bool)
    else:
        readings[i] = convert(readings[i], abs)
print(readings)

Sample Run:

[40.5, False, True, True, 91]
FindThe printed list, item by item, and why two of its items are the word True.
Given
  • The readings are [-40.5, 0, 12.25, -3.75, 91.6].

  • The branches use int by default for readings above 50, bool for readings above minus 10, and abs otherwise.

Solution

Which branch each reading takes

$$-40.5 \le -10$$

Neither above 50 nor above minus 10, so the else branch applies abs and the item becomes 40.5, still a float.

$$0 > -10\ \text{and}\ 0 \le 50$$

Middle branch, so bool is applied, and bool(0) is False. Zero is the only number that gives False, which is the point of putting it in the data.

$$12.25 > -10,\ -3.75 > -10$$

Both take the middle branch and both are non zero, so both become True. The values are lost entirely, which is what applying bool means.

$$91.6 > 50$$

First branch, and the default is used because the call passes one argument. int(91.6) is 91, throwing the fraction away rather than rounding to 92.

Why the default is enough for the first branch

$$\texttt{convert(readings[i])}\;\equiv\;\texttt{convert(readings[i], int)}$$

A parameter with a default is optional, and the default here is a function object rather than a number.

Answer $$\boxed{\texttt{[40.5, False, True, True, 91]}}$$
Check

Independent check on the mix of types: the printed list has a float, two booleans and an int in it, which is four different types in five items.

Reading this kind of trace, do the branch test first and the conversion second. Most lost marks here come from applying the right function to the wrong item.

Checkpoint
§06.6 — one function used twice, on two different tools

Thirty seconds. A higher order function is called three times, once with a function written here and once with a built in one.

def add3(n):
    """Assumes n is a number. Returns n plus 3."""
    return n + 3

def twice(f, v):
    """Assumes f is a function of one argument. Returns f of f of v."""
    return f(f(v))

print(twice(add3, 10))
print(twice(abs, -5))
g = add3
print(g(1), twice(g, 1))
Find(a) Write the three lines this prints.
Given
  • add3 adds three to its argument.

  • twice(f, v) hands back f(f(v)).

IPython console
Hint 1/4

Work the inner call out first in each case, exactly as with any nested call, and remember that the last line prints two things.

Hint 2/4

f(f(v)) applies the tool to the result of applying the tool. A name assigned from a function name is a second name for the same function.

Hint 3/4

Here the calls are twice(add3, 10), twice(abs, -5) and, after g = add3, both g(1) and twice(g, 1).

Hint 4/4

Adding three twice moves 10 by six; applying abs twice is the same as applying it once; and the last line is a single application followed by a double one.

Show solution

The two nested calls

$$\texttt{add3(add3(10))} = \texttt{add3(13)} = 16$$

Inner first, which is the same rule as any call inside a call.

$$\texttt{abs(abs(-5))} = \texttt{abs(5)} = 5$$

The inner call already made it positive, so the outer one changes nothing.

The alias for a function

$$\texttt{g = add3}$$

No brackets, so this is the same function under a second name; nothing is called on this line.

$$\texttt{g(1)} = 4,\ \texttt{twice(g, 1)} = 7$$

One application adds three and two applications add six, so the last line is two numbers on one line.

Answer $$\boxed{16\;/\;5\;/\;\texttt{4 7}}$$
Check

Check the first line a different way: applying a function that adds three twice is the same as adding six once, and 10 plus 6 is 16.

⚠ Calling the function instead of passing it

Every other use of a function name in the course has had brackets after it.

wrong$$\texttt{apply\_to\_each(L, abs())}$$
right$$\texttt{apply\_to\_each(L, abs)}$$
⚠ Assigning from a higher order function that changes the list

It looks like a mapping operation, and in other languages that is what it is.

wrong$$\texttt{L = apply\_to\_each(L, round)}\;\Rightarrow\;\texttt{None}$$
right$$\texttt{apply\_to\_each(L, round)}\;\text{on a line of its own}$$
⚠ Storing the result where the function was wanted

The dictionary of operations is usually written by copying a line that called one of them.

wrong$$\texttt{table = \{'add': add(3, 8)\}}$$
right$$\texttt{table = \{'add': add\}}$$
⚠ Passing a function's name as a string

The user types a word, so the word feels like the thing to pass on.

wrong$$\texttt{apply\_to\_each(L, 'abs')}$$
right$$\texttt{apply\_to\_each(L, abs)}\;\text{or look the string up in a dictionary first}$$

6.7Changing a structure while a loop is walking through it

A for loop over a list counts positions, so removing items underneath it makes the positions move and some items are never seen.

Removing items is now possible and walking items was possible from the start, and doing both at once is where the marks go missing.

RuleRule 6.7: three safe patterns, and one that is not
Conditions
  • A for loop over a list keeps a hidden position counter and asks the list for the item at that position each time.

  • So removing an item inside such a loop slides the later items one place left while the counter moves one place right, and the item that slid into the vacated place is stepped over.

  • Safe pattern one: walk a clone and change the original. for mark in group[:] reads from the copy, so the positions it counts never move.

  • Safe pattern two: a while loop with a counter you advance yourself, advancing only when nothing was removed. This is the pattern a past exam solution used.

  • Safe pattern three: build a new list of the items you want to keep and hand that back, changing nothing. Usually the clearest of the three, and the only one that leaves the caller's list alone.

  • For a dictionary the rule is stricter: changing the number of pairs during a walk over it stops the program with a RuntimeError rather than silently skipping.

  • Adding items inside a loop over the same list is worse still, since the loop can then keep finding new work and never end.

$$\boxed{\text{remove inside}\ \texttt{for}\ \text{over the same list}\Rightarrow\ \text{items skipped};\quad \text{clone, or}\ \texttt{while},\ \text{or build new}}$$

The loop is counting places, not items, so anything that moves the items between two passes moves them behind the loop's back. Either walk something that cannot move, or move the counter yourself.

Looks like this, but is not

The loop below removes every mark below fifty, and here it really does. So the pattern is fine.

marks = [30, 75, 40, 90, 20]
for mark in marks:
    if mark < 50:
        marks.remove(mark)
print(marks)

Sample Run:

[75, 90]

The answer is right and the program is still wrong. In this list the three low marks are separated by high ones, so every time the items slide left the item skipped is one that was going to be kept anyway.

The same six lines, on a list where two low marks are neighbours

The unlucky input. Two marks below fifty are next to each other, and one of them survives.

marks = [30, 40, 75, 20, 90]
for mark in marks:
    if mark < 50:
        marks.remove(mark)
print(marks)

Sample Run:

[40, 75, 90]
FindWhich mark survives, and at which pass it was skipped.
Given
  • The marks are [30, 40, 75, 20, 90].

  • The loop removes any mark below fifty.

Solution

Follow the counter, not the items

$$\text{pass 1: position }0,\ \text{item }30$$

Below fifty, so it goes. The list becomes [40, 75, 20, 90] and everything after the 30 has moved one place left.

$$\text{pass 2: position }1,\ \text{item }75$$

The counter has moved to 1, but position 1 now holds 75, because the 40 slid into position 0 behind the counter.

The rest of the walk

$$\text{pass 3: position }2,\ \text{item }20$$

Below fifty, removed, and the list becomes [40, 75, 90] with length 3.

$$\text{pass 4: position }3$$

The length is now 3, so there is no position 3 and the loop stops.

Answer $$\boxed{\texttt{[40, 75, 90]}\quad\text{with}\ 40\ \text{below the limit}}$$
Check

An independent check that the bug is about positions rather than about the number 40: reverse the input to [90, 20, 75, 40, 30] and the survivor changes, while passing_only on either input gives a list with nothing below fifty in it.

When a removal loop gives a nearly right answer, count how many items it actually tested. It is always fewer than the length it started with.

The three patterns that work, on the same unlucky list

First the while loop with a counter advanced by hand, which also hands back what it took out. This is the shape a past exam solution used for exactly this job.

def take_out_failing(marks):
    """Assumes marks is a list of numbers.
    Removes every mark below 50 from the list it was given and returns a new
    list holding the removed marks, in the order they were found.
    """
    failed = []
    i = 0
    while i < len(marks):
        if marks[i] < 50:
            failed.append(marks.pop(i))
        else:
            i = i + 1
    return failed

group = [30, 40, 75, 20, 90]
print('before:', group)
out = take_out_failing(group)
print('after :', group)
print('failed:', out)

Sample Run:

before: [30, 40, 75, 20, 90]
after : [75, 90]
failed: [30, 40, 20]

Then the version that builds a new list and changes nothing.

def passing_only(marks):
    """Assumes marks is a list of numbers.
    Returns a new list of the marks that are 50 or more and leaves the
    list it was given untouched.
    """
    kept = []
    for mark in marks:
        if mark >= 50:
            kept.append(mark)
    return kept

group = [30, 40, 75, 20, 90]
print('kept  :', passing_only(group))
print('before:', group)

Sample Run:

kept  : [75, 90]
before: [30, 40, 75, 20, 90]

And the shortest fix to the broken loop, which is to walk a clone.

group = [30, 40, 75, 20, 90]
for mark in group[:]:
    if mark < 50:
        group.remove(mark)
print(group)

Sample Run:

[75, 90]
FindWhat each one leaves in the original list, what each hands back, and which to reach for.
Given
  • All three start from [30, 40, 75, 20, 90].

  • The limit is fifty in all three.

Solution

Pattern two, the while loop

$$\texttt{if marks[i] < 50: failed.append(marks.pop(i))}$$

Pop both removes and reports, so the removed mark can be kept. The counter is deliberately not advanced here, because the item that slid into position i has not been tested yet.

$$\texttt{else: i = i + 1}$$

The only place the counter moves. This single line is the whole difference from the broken version.

$$\text{result: }\texttt{[75, 90]}\ \text{and}\ \texttt{[30, 40, 20]}$$

Both halves of the answer are available, which the other two patterns cannot both give at once.

Pattern three, build a new list

$$\texttt{kept.append(mark)}\ \text{when}\ \texttt{mark >= 50}$$

Nothing is removed anywhere, so no position ever moves and the loop can be the simple one over the items.

$$\text{the original prints unchanged}$$

This is a promise the other two cannot make, and it is the one to choose when the caller still needs the data.

Pattern one, walk a clone

$$\texttt{for mark in group[:]}$$

Four characters, and the loop now counts positions in a list that nobody is changing.

$$\text{result: }\texttt{[75, 90]}$$

Correct, and with no record of what was removed. Reach for this when a broken loop has to be fixed with the least possible editing.

Answer $$\boxed{\text{all three give}\ \texttt{[75, 90]};\ \text{only the}\ \texttt{while}\ \text{also gives}\ \texttt{[30, 40, 20]}}$$
Check

Independent check across the three: the three surviving lists are identical and the removed marks add up to 30 plus 40 plus 20, which is 90, while the original total was 255 and the surviving total is 165. 165 plus 90 is 255, so nothing was lost or invented by any of them.

The clone costs one extra list of five items. The while loop costs no extra memory and one more line of thinking.

If the question says remove, the while loop or the clone. If it says return the ones that, build a new list.

The same mistake on a dictionary, where it stops the program

Dropping the parts that are out of stock, written the way the list version was written.

stock = {'nut': 0, 'bolt': 12, 'washer': 0}
for part in stock:
    if stock[part] == 0:
        stock.pop(part)
print(stock)

Sample Run:

Traceback (most recent call last):
  File "stock_clean.py", line 2, in <module>
    for part in stock:
RuntimeError: dictionary changed size during iteration
FindWhy this one reports an error rather than skipping quietly, and the fix.
Given
  • The dictionary is {'nut': 0, 'bolt': 12, 'washer': 0}.

  • The loop removes any pair whose value is 0.

Solution

What the error is saying

$$\texttt{RuntimeError: dictionary changed size during iteration}$$

A dictionary walk checks that the number of pairs has not changed, so the failure is loud.

The fix, which is pattern one again

$$\texttt{for part in sorted(stock.keys())}$$

Sorting builds a new list of the keys, so the walk is over something nobody is changing, and the removals land in the dictionary as intended.

$$\texttt{list(stock.keys())}$$

The same fix without imposing an order, for when the order does not matter.

Answer $$\boxed{\text{no output, a RuntimeError};\ \text{fix by walking}\ \texttt{sorted(stock.keys())}}$$
Check

A check on which of the two objects is the problem: reading stock[part] inside the loop is fine, and stock[part] = 0 would also be fine, because neither changes the number of pairs.

The dictionary tells you off and the list does not. Treat the loud error as the lucky case and apply the same fix to both.

Checkpoint
§06.7 — removing the even numbers, one pass too few

Thirty seconds, and the answer is not what the code says it wants.

items = [1, 2, 4, 5, 6]
for x in items:
    if x % 2 == 0:
        items.remove(x)
print(items)
Find(a) Write the one line this prints.
Given
  • The list is [1, 2, 4, 5, 6].

  • The loop removes any even item.

IPython console
Hint 1/4

Write the list out again after each removal, with the position the loop is about to look at marked. The question is which positions the loop ever reaches.

Hint 2/4

The loop counts positions. A removal slides everything after it one place left, and the counter still moves one place right.

Hint 3/4

Here the two evens 2 and 4 are neighbours, so when the 2 goes the 4 slides into its place at position 1 while the counter moves to position 2.

Hint 4/4

One even number survives, and it is the one that was next to the first one removed.

Show solution

Pass by pass, with the list rewritten each time

$$\text{position }0:\ 1\ \text{kept}$$

Odd, so nothing moves and the picture is unchanged.

$$\text{position }1:\ 2\ \text{removed}\Rightarrow\texttt{[1, 4, 5, 6]}$$

The 4, 5 and 6 all slide one place left. The 4 is now at position 1, which the counter has already passed.

$$\text{position }2:\ 5\ \text{kept}$$

This is where the 4 would have been tested, and the 5 is standing there instead.

$$\text{position }3:\ 6\ \text{removed}\Rightarrow\texttt{[1, 4, 5]}$$

Length is now 3, so position 4 does not exist and the loop ends.

Answer $$\boxed{\texttt{[1, 4, 5]}}$$
Check

Check with the fix rather than by repeating the trace: walking items[:] on the same data gives [1, 5], which is what the code was trying to say.

⚠ Removing from a list inside a for loop over it

It reads exactly like the sentence in the question, and on many inputs it gives the right answer.

wrong$$\texttt{for mark in marks: if mark < 50: marks.remove(mark)}$$
right$$\texttt{for mark in marks[:]: if mark < 50: marks.remove(mark)}$$
⚠ Advancing the counter after a removal in a while loop

Every other while loop in the course advances its counter on every pass, so the else looks unnecessary.

wrong$$\texttt{if marks[i] < 50: marks.pop(i)}\ \text{then}\ \texttt{i = i + 1}$$
right$$\texttt{else: i = i + 1}\ \text{so the counter waits when something was removed}$$
⚠ Removing pairs from a dictionary while walking it

The list version of the same loop runs without complaining.

wrong$$\texttt{for part in stock: stock.pop(part)}$$
right$$\texttt{for part in sorted(stock.keys()): stock.pop(part)}$$
⚠ Walking by index over a shrinking list

range(len(L)) is the standard walk, and it is worked out once before the loop starts.

wrong$$\texttt{for i in range(len(marks)): marks.pop(i)}\;\Rightarrow\;\text{IndexError}$$
right$$\texttt{while i < len(marks):}\ \text{with the counter advanced by hand}$$
Choosing between a tuple, a list, a dictionary and a range

Before writing any code for a question that mentions more than one value. Five minutes here saves the rewrite that happens when a list turns out to need names instead of positions.

  1. Ask whether the values are looked up by name or by position

    By name, such as a student number or a part code, means a dictionary. By position, or in an order that matters, means one of the sequences.

  2. Ask whether the group will grow or shrink

    If items will be added or taken away, it is a list. If the group is a fixed record, such as a course name with a mark, it is a tuple.

  3. Ask whether you are storing values or counting

    A sequence of consecutive whole numbers that is only being walked over never needs storing at all: that is range. Turning a range into a tuple or a list is only worth it when you have to index it repeatedly or keep it after the loop.

  4. Decide what one item looks like, then write that down

    Say it as a sentence: key is a student number, value is a list, item is a tuple of a course and a mark.

Where it goes wrong
  • Two parallel lists, one of names and one of marks, kept in step by hand. Every lookup is then a search, and the day one list is sorted the other is wrong.

  • A dictionary where the key is never used to look anything up, only walked. That is a list of pairs written the long way.

  • A tuple for a group that the question later asks you to extend, which forces a rebuild on every addition.

  • Storing a range as a list of a thousand numbers when the loop only needed to walk it once.

Deciding whether a function should change its argument or hand something back

Every time you write a function whose parameter is a list or a dictionary. The wrong choice still runs, and it costs marks because the caller in the question expects the other one.

  1. Read the question for the word the marker will read

    Removes, adds, sorts and updates all describe a change to the thing that was passed in. Returns, gives and finds all describe a new value.

  2. If it changes the argument, return nothing and say so

    No return statement at all, and a docstring that says changes the list it was given. The caller then has to use the argument afterwards, never the value of the call, because the value is None.

  3. If it returns a new value, do not touch the argument

    Build a new list or dictionary inside the body and return it. Read the parameter as much as you like and assign to nothing that belongs to the caller.

  4. When both are needed, use pop and return the removals

    A question that says remove the failing marks and return them wants a body that changes the list and also hands back a new list of what it took.

Where it goes wrong
  • Changing the argument and also returning it, so the caller cannot tell which of the two happened and the docstring has to say both.

  • Returning a slice of the argument and believing the caller's list is safe when the items are themselves lists.

  • Writing a function that only prints, so nothing can be tested and no caller can use the answer.

  • Assigning to the parameter name, which changes nothing outside and looks from inside the body exactly like a change that works.

From a lab sheet of named functions to a finished file with a sample run

Any lab question that lists functions with names and then says write a script that does the following. The shape of the answer is fixed and marks are lost on the shape rather than on the logic.

  1. Write the headers first, all of them, with docstrings and no bodies

    One def per named job, in the order the sheet lists them, each with the parameters the sheet mentions and a docstring saying what it assumes and what it returns.

  2. Decide, for each one, which of the two families it is in

    Add student changes a dictionary and returns nothing. Find student returns a value or None. Deciding this before the bodies is what keeps the script at the bottom short, because the script then knows whether to print the call or the argument.

  3. Fill the bodies from the smallest one up

    The function with no other function inside it goes first, because it can be tested alone with one print. A function that uses another one is then tested with the first already known to work, which is the only way to tell which of the two is wrong when the answer comes out odd.

  4. Write the script, and keep every print in it

    The functions stay silent and the script does the talking, unless the sheet asks for a message from inside, as the error messages of a menu usually are.

  5. Match the sample run character by character

    Compare with the sheet's sample run: the wording of the prompts, the space after a colon, the number of decimals, the text of an error message.

Where it goes wrong
  • Printing inside the functions and then discovering the script has nothing to print.

  • A menu whose loop reads the choice once, above the loop, so the program either runs one operation or never stops.

  • Reading a number with input and comparing it to a number, without int or float around the call.

  • Functions named something other than what the sheet said, which costs marks even when every body is right.

Putting the marks in order, in place

A function whose job is to leave the caller's list in order.

def order_in_place(marks):
    """Assumes marks is a list of numbers.
    Sorts the list it was given, smallest first, and returns nothing.
    """
    marks.sort()

group = [72, 45, 91, 60]
print('before:', group)
answer = order_in_place(group)
print('after :', group)
print('the call handed back', answer)

Sample Run:

before: [72, 45, 91, 60]
after : [45, 60, 72, 91]
the call handed back None
FindWhat the caller has to look at afterwards.
Given
  • The marks are [72, 45, 91, 60].

  • The body is a single call to sort.

Solution

The body changes the object

$$\texttt{marks.sort()}$$

The parameter is a reference to the caller's list, so sorting it here sorts the list the caller can still see.

$$\text{no}\ \texttt{return}$$

There is nothing to hand back. Adding return marks would let a caller write a line that looks like the other family and is not.

Answer $$\boxed{\texttt{group} = \texttt{[45, 60, 72, 91]},\ \text{the call is worth}\ \texttt{None}}$$
Check

The printed None is the evidence. If the call had been worth anything, the third line would show a list and the caller would have two ways of reading the same answer.

Handing back the marks in order, leaving the original alone

The same job described the other way round, and the body changes by one word.

def order_copy(marks):
    """Assumes marks is a list of numbers.
    Returns a new list with the same numbers in order, smallest first, and
    leaves the list it was given untouched.
    """
    return sorted(marks)

group = [72, 45, 91, 60]
ordered = order_copy(group)
print('the new list:', ordered)
print('the original:', group)
print('same object? ', ordered is group)

Sample Run:

the new list: [45, 60, 72, 91]
the original: [72, 45, 91, 60]
same object?  False
FindWhat the caller has to look at afterwards, and what is left of the original order.
Given
  • The marks are [72, 45, 91, 60].

  • The body is a single call to sorted.

Solution

The body builds a new object

$$\texttt{return sorted(marks)}$$

sorted reads the list and builds another, so the caller's list is never written to and the answer has to leave through the return.

$$\texttt{ordered is group}\;\rightarrow\;\texttt{False}$$

Two objects, which is the whole difference. The printed original still shows the order it was given in.

Answer $$\boxed{\texttt{ordered} = \texttt{[45, 60, 72, 91]},\ \texttt{group}\ \text{unchanged}}$$
Check

Both prints are needed as evidence: the new list is in order and the old one is not, so exactly one object was sorted and it was not the caller's.

Both functions put four marks in order and the difference is invisible in their names, so it has to be read off two places: whether there is a return, and whether the body used sort or sorted.

How to tell them apart

Look at what the caller uses after the call. If the next line reads the argument, the function belongs to the first family and must not return anything. If the next line reads the value of the call, the function belongs to the second and must not touch the argument.

Scaffolding comes off
The common skeleton
  1. Name what one item has to be compared with. Most questions of this kind are about an item and its neighbour, so the loop variable is an index rather than an item.

  2. Set the counting name to 0 above the loop, so that it survives every pass.

  3. Choose the range of positions so that every comparison the body makes exists. Comparing with the item before means starting at 1; comparing with both neighbours means starting at 1 and stopping one early.

  4. Do the comparison in one if and change the counting name under it, not under the for.

  5. Return the counting name after the loop, never inside it.

1 · fully worked

How many items of a tuple are higher than both of their neighbours

A value is called a peak when it is greater than the item before it and greater than the item after it. Write a function that counts the peaks in a tuple of numbers, and try it on a flat tuple and on a tuple of two items as well.

def count_peaks(values):
    """Assumes values is a tuple of numbers.
    Returns how many items are greater than both of their neighbours.
    """
    peaks = 0
    for i in range(1, len(values) - 1):
        if values[i] > values[i - 1] and values[i] > values[i + 1]:
            peaks = peaks + 1
    return peaks

readings = (31, 18, 26, 12, 6, 9, 14, 7)
print(readings)
print('peaks:', count_peaks(readings))
print('flat tuple:', count_peaks((4, 4, 4, 4)))
print('two items :', count_peaks((9, 1)))

Sample Run:

(31, 18, 26, 12, 6, 9, 14, 7)
peaks: 2
flat tuple: 0
two items : 0
FindThe count, and what the two extra calls show.
Given
  • The tuple is (31, 18, 26, 12, 6, 9, 14, 7).

  • A peak must beat both of its neighbours, so the first and last items can never be peaks.

Solution

The positions that can be tested at all

$$\texttt{for i in range(1, len(values) - 1)}$$

Position 0 has nothing on its left and the last position has nothing on its right, so neither can be compared with both neighbours.

One if with two conditions

$$\texttt{values[i] > values[i - 1] and values[i] > values[i + 1]}$$

Strictly greater on both sides, which is what beats both neighbours means; >= would count a flat shoulder as a peak.

$$\texttt{peaks = peaks + 1}$$

Under the if, so only a peak counts, and the name was set to 0 above the loop so it survives the passes.

Read the answer off the data

$$26 > 18\ \text{and}\ 26 > 12$$

The first peak. 18 is not one because 31 is on its left.

$$14 > 9\ \text{and}\ 14 > 7$$

The second peak, and 7 at the end cannot be tested at all.

The two extra calls

$$\texttt{(4, 4, 4, 4)}\;\rightarrow\;0$$

Nothing is strictly greater than a neighbour, so the strict comparison is doing its job.

$$\texttt{(9, 1)}\;\rightarrow\;0$$

range(1, 1) is empty, so the loop never runs and the function returns 0 rather than failing.

Answer $$\boxed{2\ \text{peaks}:\ 26\ \text{and}\ 14}$$
Check

Check by walking the tuple as a shape: from 31 it falls to 18, rises to 26, falls to 12 and to 6, rises to 9 and to 14, falls to 7.

Every question about an item and its neighbours is this loop with a different if. The range of positions is decided by how many neighbours the condition mentions.

2 · you write the reasoning

Same skeleton, easier question: how many items are greater than the item just before them. The steps are below with the reasons taken out. Write your own reason for each before opening it. Nothing has to be invented here; the question is only why each line is where it is.

def count_climbs(values):
    """Assumes values is a tuple of numbers.
    Returns how many items are greater than the item just before them.
    """
    climbs = 0
    for i in range(1, len(values)):
        if values[i] > values[i - 1]:
            climbs = climbs + 1
    return climbs

readings = (31, 18, 26, 12, 6, 9, 14, 7)
print('climbs:', count_climbs(readings))

Sample Run:

climbs: 3
  1. The loop starts at 1 rather than at 0.

    reasoning

    Because position 0 has nothing before it. The body reads values[i - 1], and at i equal to 0 that is values[-1], which is the last item: the comparison would succeed or fail for no reason connected to the question.

  2. It stops at the end this time, not one short of it.

    reasoning

    Because every position except the first has an item before it. The peak version had to stop early only because it also read values[i + 1], and this one never does.

  3. climbs = 0 is written above the loop.

    reasoning

    Because it has to survive all the passes. Inside the loop it would be set back to 0 each time and the answer would be 0 or 1.

  4. The comparison mentions only one neighbour.

    reasoning

    Because the question mentions one neighbour, so one comparison is the whole test. Adding the other neighbour would silently turn this into the peak question.

  5. The return is after the loop and the function prints nothing.

    reasoning

    Because the count is only complete once every position has been looked at. A return inside the loop would report after the first pass instead of after the last.

3 · find the buried error

Harder, and now the program is somebody else's. A climb is a run of items that keeps increasing, and the length of a climb is how many items are in it. The program is supposed to report the longest climb in the tuple, and for this tuple the right answer is 4, because 4, 6, 7, 8 at the end is four items long.

def longest_climb(values):
    """Assumes values is a tuple of numbers with at least one item.
    Returns the length of the longest run of items that keeps increasing.
    """
    best = 0
    run = 1
    for i in range(1, len(values)):
        if values[i] >= values[i - 1]:
            run = run + 1
        else:
            if run > best:
                best = run
            run = 1
    return best

readings = (2, 5, 9, 4, 4, 6, 7, 8)
print(longest_climb(readings))

Sample Run:

3
  1. Keep two counting names: the best climb seen so far, and the climb currently being walked.

  2. Walk the positions from 1, so that each item can be compared with the one before it.

  3. If this item continues the climb, the current run gets one longer.

  4. Otherwise the climb has ended, so compare it with the best and start a new one.

  5. After the loop, hand back the best.

the two buried errors (2)
⚠ step 3

>= counts a repeated value as a continuation. In this tuple the two 4s are next to each other, so the run 4, 4, 6, 7, 8 is measured as five items when the longest increasing run is really 4, 6, 7, 8 with four.

Increasing and not decreasing sound like the same thing in English, and the flat case is the one nobody has in their test data. The same slip turns a peak into a shoulder in the rung above.

right

Use the strict comparison: if values[i] > values[i - 1]:. Then a repeat ends the run, and the run that starts at the second 4 is counted from 1.

⚠ step 5

The final run is never compared with best, because that comparison only happens in the else branch and the else branch never runs after the last item. Here the longest climb ends at the last item, so it is the one thrown away, and the answer comes from the earlier run 2, 5, 9 instead.

The loop is written from the inside out and the comparison feels finished once it is in the body. It only shows on data where the best run reaches the end, and half the time a random test does not.

right

Repeat the comparison once more after the loop, before the return: if run > best: best = run, then return best.

4 · the bare problem
§06.7 — the same skeleton, turned upside down

No scaffolding this time. A value is called a valley when it is smaller than the item before it and smaller than the item after it. Write a function count_valleys(values) that takes a tuple of numbers and returns how many valleys it has, with a docstring, and call it on the tuple below and on (5, 5, 5).

Find
  1. (a) Write the function and the two calls.

  2. (b) Write the two numbers the program prints.

Given
  • The tuple to test is (12, 4, 9, 9, 2, 6, 1, 8).

  • A valley must be smaller than both of its neighbours, so the first and last items can never be valleys.

Hint 1/4

This is the peak question with the comparisons turned round. Decide first which positions can be tested at all, because that part does not change.

Hint 2/4

Walk range(1, len(values) - 1), keep a counter set to 0 above the loop, and put one if with two strict comparisons inside it.

Hint 3/4

On (12, 4, 9, 9, 2, 6, 1, 8) the positions to test are 1 to 6, and the items there are 4, 9, 9, 2, 6 and 1.

Hint 4/4

Three of those six items are smaller than both of their neighbours, and the flat tuple has none.

Show solution

Reuse the skeleton, change only the comparisons

$$\texttt{for i in range(1, len(values) - 1)}$$

Both neighbours are mentioned in the condition, so both ends have to be left out; this is the same line as in the peak version.

$$\texttt{values[i] < values[i - 1] and values[i] < values[i + 1]}$$

Only the direction of the two comparisons changes. Strict, so that the pair of equal 9s cannot produce a valley.

Count on the data

$$4 < 12\ \text{and}\ 4 < 9$$

First valley.

$$2 < 9\ \text{and}\ 2 < 6$$

Second valley, with the second 9 on its left.

$$1 < 6\ \text{and}\ 1 < 8$$

Third valley, and the 8 at the end cannot be tested itself.

The flat tuple

$$\texttt{(5, 5, 5)}\;\rightarrow\;0$$

One position to test and no strict inequality holds there, so the answer is 0 rather than an error.

Answer $$\boxed{3\ \text{valleys, then}\ 0}$$
Check

Independent check from the shape of the data: 12 falls to 4, rises to 9, stays at 9, falls to 2, rises to 6, falls to 1, rises to 8.

Full exam-style question

Averages of a dictionary of lists, without changing the inputexam format

Exam shape, and worth about fifteen marks on the paper this page was written against. Write a function averages(marks) that takes a dictionary whose keys are student names and whose values are lists of numbers, and returns a NEW dictionary from each name to the average of that student's marks. The dictionary passed in must not be changed.

def averages(marks):
    """Assumes marks is a dictionary of student name to a list of numbers,
    and that no list is empty.
    Returns a NEW dictionary of name to the average of that student's marks.
    The dictionary given as the parameter is not changed.
    """
    out = {}
    for name in marks:
        total = 0
        for mark in marks[name]:
            total = total + mark
        out[name] = total / len(marks[name])
    return out

book = {'Ada': [80, 90, 100], 'Bora': [55, 60], 'Cem': [70]}
means = averages(book)
print(means)
print('the input is still', book)
print('Bora average is', format(means['Bora'], '.2f'))

Sample Run:

{'Ada': 90.0, 'Bora': 57.5, 'Cem': 70.0}
the input is still {'Ada': [80, 90, 100], 'Bora': [55, 60], 'Cem': [70]}
Bora average is 57.50
FindThe function, the dictionary it returns, and the evidence that the input survived.
Given
  • The input is {'Ada': [80, 90, 100], 'Bora': [55, 60], 'Cem': [70]}.

  • Each value is a list of numbers of any length, and no list is empty.

  • The input dictionary must be unchanged when the function returns.

Solution

Say the shape out loud before writing anything

$$\text{in: name}\mapsto\text{list of numbers};\quad \text{out: name}\mapsto\text{one number}$$

The keys are the same in both, so the loop is over the keys of the input and each pass writes one pair into the output.

$$\texttt{out = \{\}}$$

The word NEW in the question is a requirement, and building an empty dictionary in the body is what satisfies it.

The inner total, and why it is inside the outer loop

$$\texttt{total = 0}\;\text{inside the outer loop}$$

Each student needs a fresh total. Setting it above the outer loop is the single commonest way to lose the marks on this question, and it fails silently, since the first student's answer is still right.

$$\texttt{for mark in marks[name]: total = total + mark}$$

The value is a list, so it can be walked directly; no index is needed because nothing here is about position.

Divide by the right count

$$\texttt{out[name] = total / len(marks[name])}$$

The length of that student's list, not of the dictionary. Both are numbers and both are available, so the wrong one runs perfectly and gives nonsense.

$$80 + 90 + 100 = 270,\ 270/3 = 90.0$$

Division always gives a float, which is why the printed averages have a decimal point even where they divide exactly.

Show that the input survived

$$\texttt{print('the input is still', book)}$$

The body never assigned to marks or to any of its values, so the lists print exactly as they went in.

$$\texttt{format(means['Bora'], '.2f')}\;\rightarrow\;\texttt{57.50}$$

Reading one value back out of the returned dictionary, and formatting it the way a report would; the stored value is still the full float.

Answer $$\boxed{\texttt{\{'Ada': 90.0, 'Bora': 57.5, 'Cem': 70.0\}},\ \text{input unchanged}}$$
Check

Check each average independently: Ada's three marks are evenly spaced around 90, so their mean is 90 exactly; Bora's two differ by 5 so their mean is 57.5, halfway between 55 and 60; Cem has one mark so his average is that mark.

Two nested loops, one dictionary built, nothing copied. A version that cloned each list first would do the same arithmetic and allocate a list per student for nothing, since reading a list never changes it.

When a question says returns a new dictionary and must not change the input, both halves are marked.

Practice

A · concept 3 questions
1§06.1 — what immutable actually promises

A tuple holds a string and a list. Decide whether the claim below is true, and give the reason in one sentence.

record = ('lab', [55, 90])
print(record)
record[1].append(70)
print(record)

The claim: the second print shows the same line as the first, because record is a tuple and tuples cannot change.

Find(a) True or false, with the reason.
Given
  • record is ('lab', [55, 90]).

  • The middle line appends 70 to the list inside the tuple.

Hint 1/4

Separate two questions: can the tuple be made to hold different things, and can the things it holds be changed. The claim answers the first and the code does the second.

Hint 2/4

A tuple refuses to have an item replaced and refuses to change length. It makes no promise at all about the objects its items point at.

Hint 3/4

Here the second slot points at the list [55, 90], and append is an operation that changes a list in place.

Hint 4/4

False: the tuple still holds the same three references, and one of them now points at a longer list.

Show solution

Ask what the slot holds

$$\texttt{record[1]}\;\rightarrow\;\texttt{[55, 90]}$$

A list, which is the one type here with operations that change it in place.

$$\texttt{.append(70)}$$

Applied to the list, not to the tuple, so the tuple is never asked to do anything it refuses.

Answer $$\boxed{\text{False}}$$
Check

Independent check: len(record) is 2 both times and len(record[1]) goes from 2 to 3. The tuple kept its shape and the list grew, which is exactly what the claim denies.

2§06.2 — a sorted copy that is not a copy and is not sorted

A program wants to keep the marks in the order they were given and also have them sorted. Decide whether the claim below is true, and give the reason in one sentence.

marks = [70, 45, 90]
best = marks.sort()
print(best)
print(marks)

The claim: after this runs, best holds [45, 70, 90] and marks is still in its original order.

Find(a) True or false, with the reason.
Given
  • marks is [70, 45, 90].

  • best is assigned from a call to sort on the list.

Hint 1/4

Two claims are being made at once, about two different names. Check them separately and remember that this operation belongs to one of the two families.

Hint 2/4

sort rewrites the list it is called on and hands back None. sorted builds a new list and leaves the original alone.

Hint 3/4

Here the call is marks.sort() with the result assigned to best, on the list [70, 45, 90].

Hint 4/4

False twice over: the name holds nothing useful and the list is no longer in its original order.

Show solution

What the call hands back

$$\texttt{marks.sort()}\;\rightarrow\;\texttt{None}$$

Every operation of the first family hands back None, which is why assigning from one is always a mistake.

What the call did

$$\texttt{marks} = \texttt{[45, 70, 90]}$$

The sorting happened, in the caller's list, so the original order is not recoverable from anywhere in this program.

Answer $$\boxed{\text{False}}$$
Check

A check that separates the two failures: print(type(best)) reports NoneType rather than list, and print(marks == [70, 45, 90]) reports False. Each print settles one half of the claim.

3§06.3 — two lists that look identical on the screen

Two names are built from the same three numbers, separately. Decide whether the claim below is true, and give the reason in one sentence.

first = [10, 20]
second = [10, 20]
print(first, second)
print(first == second, first is second)
first.append(30)
print(first, second)

The claim: the two lists print identically on the first line, so appending to one of them will change the other.

Find(a) True or false, with the reason.
Given
  • first and second are each written out as [10, 20].

  • Nothing is assigned from one name to the other anywhere.

Hint 1/4

Ask how many list objects were built by the first two lines, and then which of them the append can reach.

Hint 2/4

Aliases are made by assigning one name from another. Two separate list literals build two separate objects, whatever they contain.

Hint 3/4

Here each of the two lines has its own [10, 20] written out, and the append is called on first.

Hint 4/4

False: two objects, so the append reaches only one of them, which the last line shows.

Show solution

Count the objects

$$\texttt{first = [10, 20]},\ \texttt{second = [10, 20]}$$

Two list literals, so two objects are built. Nothing here copies a reference from one name to the other.

$$\texttt{first == second}\;\rightarrow\;\texttt{True},\ \texttt{first is second}\;\rightarrow\;\texttt{False}$$

Contents match, identities do not, which is precisely the distinction the claim ignores.

Send the append somewhere

$$\texttt{first.append(30)}$$

It reaches the object first names and there is no route from there to the other one.

Answer $$\boxed{\text{False}}$$
Check

Turn it round for the check: write second = first instead of the second literal and rerun. Then is prints True and the last line shows the 30 in both, which is the behaviour the claim was describing, and it needed that assignment to happen.

B · computation 5 questions
1§06.1 — a tuple inside a tuple, joined and repeated

Four prints, and the second tuple has the first one as its own first item.

t = ('cs', 115)
u = (t, 'lab')
print(u)
print(u[0][1] + 1)
print(u + t)
print(2 * t[1:])
Find(a) Write the four lines this prints.
Given
  • t is ('cs', 115) and u is (t, 'lab').

  • The last line repeats a slice of t.

IPython console
Hint 1/4

Write down what u is as a picture of two slots before answering anything, and keep in mind that one of those slots holds a tuple rather than a value.

Hint 2/4

Nesting shows up as brackets inside brackets. u[0][1] reads the second item of whatever is in the first slot. + joins at the top level only, and * repeats.

Hint 3/4

Here u[0] is ('cs', 115), so u[0][1] is 115, and t[1:] is (115,) because a slice of a tuple is a tuple.

Hint 4/4

The third line has four items and the fourth has two, both of them the same number.

Show solution

Build u as a picture

$$\texttt{u}\;=\;\text{two slots, the first holding all of}\;\texttt{t}$$

Two slots: the first holds the whole of t, the second a string. This is what the first printed line shows.

Read through two layers

$$\texttt{u[0][1]} = 115,\ +1 = 116$$

First bracket picks the tuple, second picks its second item, and only then is it a number that can be added to.

Join at the top level

$$\texttt{u + t}\;\rightarrow\;\text{four items}$$

Joining puts the items of the second tuple after the items of the first; it does not look inside either of them, so the nested tuple stays nested.

Slice then repeat

$$\texttt{t[1:]} = \texttt{(115,)},\ 2\times\;\rightarrow\;\texttt{(115, 115)}$$

A slice of a tuple is a tuple even with one item in it, so * repeats the sequence rather than multiplying the number.

Answer $$\boxed{4\ \text{items in line three},\ \texttt{(115, 115)}\ \text{in line four}}$$
Check

Check the third line by counting: len(u) is 2 and len(t) is 2, so the join has 4 items, and len(u + t) would print 4.

2§06.3 — six methods, and one of them reports something

A list of parts is changed six times and printed along the way. The last line asks for an index after a removal.

parts = ['nut', 'bolt']
parts.append('washer')
parts.insert(1, 'pin')
print(parts)
taken = parts.pop()
print(taken, parts)
parts.extend(['clip', 'nut'])
print(parts, parts.count('nut'))
parts.remove('nut')
print(parts, parts.index('nut'))
parts.sort()
print(parts)
Find(a) Write the five lines this prints.
Given
  • The list starts as ['nut', 'bolt'].

  • 'nut' occurs twice in the middle of the run.

IPython console
Hint 1/4

Rewrite the whole list after every line that changes it. The two lines that print two things are asking a question about the list as it stands at that moment.

Hint 2/4

insert(i, e) pushes the rest right, pop() with no argument takes the last item and hands it back, remove(e) deletes the first match by value, and index(e) reports where the first match is now.

Hint 3/4

Here the order is append washer, insert pin at 1, pop the last, extend with clip and nut, count the nuts, remove the first nut, then ask for the index of nut and finally sort.

Hint 4/4

The count is 2, and after the removal the surviving nut is the last item of four, so its index is 3.

Show solution

Adding, three ways

$$\texttt{append('washer')}\;\rightarrow\;\texttt{['nut', 'bolt', 'washer']}$$

One item at the end.

$$\texttt{insert(1, 'pin')}\;\rightarrow\;\texttt{['nut', 'pin', 'bolt', 'washer']}$$

Position 1, and bolt and washer each move one place right; nothing is overwritten.

Taking the last one off

$$\texttt{pop()}\;\rightarrow\;\texttt{washer}$$

No argument means the last item, and it is handed back as well as removed, which is why the printed line has two parts.

Extending and counting

$$\texttt{extend(['clip', 'nut'])}\;\rightarrow\;\text{five items}$$

Two items added, one at a time, so there are now two nuts: the original at index 0 and the new one at index 4.

$$\texttt{count('nut')} = 2$$

Counting changes nothing, so both nuts are still there when the next line runs.

Removing, then asking where

$$\texttt{remove('nut')}\;\rightarrow\;\texttt{['pin', 'bolt', 'clip', 'nut']}$$

The first match only. Everything after it slides one place left, which is what moves the second nut from index 4 to index 3.

$$\texttt{index('nut')} = 3$$

The surviving nut is the last of four items, so the index is 3.

Sorting

$$\texttt{sort()}\;\rightarrow\;\texttt{['bolt', 'clip', 'nut', 'pin']}$$

Strings sort alphabetically and the list is rewritten in place, so the printed line is the list itself and not a copy.

Answer $$\boxed{\text{count}\ 2,\ \text{index}\ 3,\ \text{sorted}\ \texttt{['bolt', 'clip', 'nut', 'pin']}}$$
Check

Check the lengths along the way: 2, 3, 4, 3, 5, 4, 4. Each step changes by exactly what its operation promises, and the two lines that only asked questions did not change it at all.

3§06.4 — one function that reaches out, one that does not

Two functions with bodies of one line each, called on the same list, and a third print that uses the value of a call.

def add_bonus(marks):
    """Assumes marks is a list of numbers.
    Adds a 5 mark bonus to the end of the list it was given, returns nothing.
    """
    marks.append(5)

def replace_marks(marks):
    """Assumes marks is a list of numbers.
    Binds its own name to a new list and returns nothing.
    """
    marks = [0, 0]

group = [60, 70]
add_bonus(group)
print(group)
replace_marks(group)
print(group)
print(add_bonus(group))
print(group)
Find(a) Write the four lines this prints.
Given
  • group starts as [60, 70].

  • add_bonus appends to its parameter and replace_marks assigns to its parameter.

IPython console
Hint 1/4

For each of the two functions, ask whether the body changes an object or only points a local name somewhere else. Only one of those reaches the caller.

Hint 2/4

append on a parameter changes the caller's list. Assigning to the parameter name rebinds a local name and does nothing outside. A body with no return hands back None.

Hint 3/4

Here add_bonus(group) appends 5, replace_marks(group) runs marks = [0, 0] inside its own frame, and the third print calls add_bonus again and prints what the call is worth.

Hint 4/4

Two of the four printed lines are the same list, the third is a single word, and the last has two fives in it.

Show solution

The body that changes an object

$$\texttt{marks.append(5)}\;\rightarrow\;\texttt{[60, 70, 5]}$$

The parameter is a reference to the caller's list, and append changes the object rather than the name.

The body that rebinds a name

$$\texttt{marks = [0, 0]}$$

A new list is built and the local name points at it. The caller's name still points at the old object, which is why the second printed line is unchanged.

The call used as a value

$$\texttt{print(add\_bonus(group))}\;\rightarrow\;\texttt{None}$$

The append runs first and then the value of the call is printed, and there is no return statement to give it one.

$$\texttt{group} = \texttt{[60, 70, 5, 5]}$$

Two calls to add_bonus have now happened, so there are two fives, and the order of the printing is what makes this line come last.

Answer $$\boxed{\texttt{[60, 70, 5]},\ \texttt{[60, 70, 5]},\ \texttt{None},\ \texttt{[60, 70, 5, 5]}}$$
Check

Check the third call against the fourth line: if printing a call could somehow prevent the body from running, the last line would show one 5.

4§06.3 — a lab shaped script that prices a basket

Write a script Sec06_B4.py that reads how many prices there are, reads that many prices, and reports the cheapest one with its position, then removes it and shows what is left. Use two functions: read_prices(count) returning a list of floats, and cheapest_index(prices) returning an int, and let only the script print. Prices are shown with two decimals and the list itself is printed as Python shows it.

Find
  1. (a) Write the two functions with their docstrings and the script that uses them.

  2. (b) Write the Sample Run for the input above, prompts included.

Given
  • The user types 4, then 18.5, 7.25, 32 and 7.25.

  • Two of the prices are equal and they are the cheapest, so a decision has to be made about which one counts.

  • read_prices takes the count as a parameter and returns a list; cheapest_index takes the list and returns an int.

Hint 1/4

Two questions the sheet has already answered for you: what each function gets and what each hands back. Write both headers and their docstrings before any body.

Hint 2/4

Build a list by starting from [] and appending inside a counted loop. To find where the smallest is, keep the index of the best so far and compare with a strict less than.

Hint 3/4

Here the four prices are 18.5, 7.25, 32 and 7.25, so the best index starts at 0 and moves once, to 1, and never moves again because the last 7.25 is not strictly smaller.

Hint 4/4

The reported position is 1, the removed price is 7.25, and three prices are left, one of which is still 7.25.

Show solution

The reading function

$$\texttt{prices = []}\;\text{then}\;\texttt{prices.append(float(input(...)))}$$

A list grown one item per pass is the standard shape, and float has to wrap the input before the append, since everything input hands back is a string.

$$\texttt{'Price ' + str(i + 1) + ': '}$$

The prompt counts from 1 for the user while the loop counts from 0, and joining strings needs str around the number.

The searching function

$$\texttt{best = 0}$$

The first item is the best until something beats it. Starting from a made up large number would need a number larger than any price, which no question can promise.

$$\texttt{if prices[i] < prices[best]: best = i}$$

Strictly less than, so a tie leaves the earlier index in place, which is the decision the docstring records.

The script

$$\texttt{where = cheapest\_index(prices)}$$

A number, so it can be used twice: once to report and once to remove.

$$\texttt{gone = prices.pop(where)}$$

Pop removes and reports in one line, which is what lets the last message name the price it took out as well as the ones left.

Answer $$\boxed{\text{index}\ 1,\ \text{removed}\ 7.25,\ \text{left}\ \texttt{[18.5, 32.0, 7.25]}}$$
Check

Check the surviving list against the input: four prices went in, three came out, and the total dropped from 65.0 to 57.75, a difference of exactly the removed 7.25.

5§06.5 — a small dictionary and four questions about it

One dictionary of parts, changed twice and then asked about twice.

stock = {'nut': 4}
stock['bolt'] = 9
stock['nut'] = stock['nut'] + 6
print(stock)
print(stock.get('pin', 0), stock.get('nut', 0))
stock.pop('bolt')
print(stock, len(stock))
for part in stock:
    print(part, 'has', stock[part])
Find(a) Write the five lines this prints.
Given
  • The dictionary starts as {'nut': 4}.

  • get is called once for a key that is missing and once for a key that is there.

IPython console
Hint 1/4

Keep the pairs written down and in the order they were added, because two of the printed lines depend on that order.

Hint 2/4

Assigning to a new key adds a pair at the end of the order; assigning to an existing key replaces the value and keeps its place.

Hint 3/4

Here nut goes from 4 to 10, bolt is added with 9, and then bolt is popped, leaving one pair.

Hint 4/4

The first line has two pairs with nut first, and the walk at the end runs once.

Show solution

Add, then replace

$$\texttt{stock['bolt'] = 9}\Rightarrow\ \texttt{\{'nut': 4, 'bolt': 9\}}$$

New key, so a second pair appears after the first.

$$\texttt{stock['nut'] = 4 + 6}\Rightarrow\ \texttt{\{'nut': 10, 'bolt': 9\}}$$

Existing key, so the value is replaced and the position in the order does not move, which is why nut still prints first.

Two safe questions

$$\texttt{get('pin', 0)} = 0$$

No such key, so the fallback comes back and nothing stops.

$$\texttt{get('nut', 0)} = 10$$

The key is there, so the fallback is ignored entirely.

Remove and walk

$$\texttt{pop('bolt')}\Rightarrow\ \texttt{\{'nut': 10\}},\ \texttt{len} = 1$$

One pair removed, and the value it handed back is not printed here because nothing was assigned from it.

$$\texttt{for part in stock}\;\rightarrow\;\texttt{nut has 10}$$

One pass, with the key in the loop variable and the value fetched as stock[part].

Answer $$\boxed{\texttt{\{'nut': 10, 'bolt': 9\}},\ \texttt{0 10},\ \texttt{\{'nut': 10\} 1},\ \texttt{nut has 10}}$$
Check

Check the arithmetic on the value: the nut count is 4 plus 6, and the two later lines that mention nut both say 10.

C · exam level 4 questions
1§06.7 — four versions of one lab function

Exam level. A question asks for a function that removes every mark below fifty from the list it is given and returns a new list of the removed marks. Four students hand in four versions.

def failing_a(marks):
    """Version A."""
    failed = []
    for mark in marks:
        if mark < 50:
            failed.append(mark)
            marks.remove(mark)
    return failed

def failing_b(marks):
    """Version B."""
    failed = []
    i = 0
    while i < len(marks):
        if marks[i] < 50:
            failed.append(marks.pop(i))
        else:
            i = i + 1
    return failed

def failing_c(marks):
    """Version C."""
    failed = []
    for mark in marks:
        if mark < 50:
            failed.append(mark)
    return failed

def failing_d(marks):
    """Version D."""
    failed = []
    for i in range(len(marks)):
        if marks[i] < 50:
            failed.append(marks.pop(i))
    return failed

Each one is called as failing_x([30, 40, 75, 20, 90]).

Find(a) Which version does both halves correctly?
Given
  • The list is [30, 40, 75, 20, 90], so two of the three low marks are next to each other.

  • The function must both change the list it was given and return the removed marks.

Hint 1/4

Two things are being asked for at once, so check each version twice: what it returns, and what the caller's list looks like afterwards.

Hint 2/4

Removing inside a for over the same list skips items. range(len(L)) is worked out once, before the loop, so it does not shrink with the list.

Hint 3/4

On [30, 40, 75, 20, 90] the correct answer is a returned [30, 40, 20] and a list left as [75, 90].

Hint 4/4

Only the version with the while loop and the conditional advance gets both of those.

Show solution

Check what each returns and what each leaves

$$\text{A: returns }\texttt{[30, 20]},\ \text{leaves }\texttt{[40, 75, 90]}$$

Both halves wrong, and both for the same reason: the 40 was never tested, so it was neither returned nor removed.

$$\text{C: returns }\texttt{[30, 40, 20]},\ \text{leaves the list as it was}$$

The return is right because nothing moved while the loop walked. Nothing moved because nothing was removed, which is the half it fails.

$$\text{D: IndexError}$$

range(len(marks)) is a fixed sequence of positions 0 to 4, and after two removals the list has only three of them.

Why B is safe

$$\texttt{while i < len(marks)}$$

The test reads the length on every pass, so it shrinks as the list does.

$$\texttt{failed.append(marks.pop(i))}\ \text{and}\ \texttt{else: i = i + 1}$$

Pop gives both halves of the answer in one line, and holding the counter still after a removal is what stops the item that slid into position i from being skipped.

Answer $$\boxed{\text{Version B}}$$
Check

A check that does not involve tracing: the marks removed should add up to 90 and the marks left to 165, whatever order they come out in.

2§06.1 — a tuple, a name for its list, and a slice

Exam level, of the kind that is worth thirty marks as a hand trace. One tuple, one extra name for the list inside it, one append and one slice.

t = (1, [2, 3], 4)
u = t[1]
u.append(5)
v = t[0:2]
print(t)
print(v)
print(len(t), len(v))

Which of the four outputs is right?

Find(a) Choose the output.
Given
  • t is (1, [2, 3], 4).

  • u is the list inside t, and v is a slice of t taken after the append.

Hint 1/4

Three questions, one per printed line: what t looks like after the append, what v holds, and what the two lengths are.

Hint 2/4

A name assigned from an item of a tuple is a second name for that item. Appending through it changes the object.

Hint 3/4

Here u is the list [2, 3] inside t, the append makes it [2, 3, 5], and v is t[0:2], which has two slots.

Hint 4/4

The list shows its 5 in both printed tuples, and the two lengths are 3 and 2.

Show solution

What u is a name for

$$\texttt{u = t[1]}$$

Not a copy. Reading an item of a tuple hands over the reference that is in the slot, so now the list has two names.

$$\texttt{u.append(5)}\Rightarrow\ \texttt{t} = \texttt{(1, [2, 3, 5], 4)}$$

The tuple is unchanged as a tuple: three slots, same three references, and the second of them points at a list that is now longer.

What the slice copies

$$\texttt{v = t[0:2]}\;\rightarrow\;\texttt{(1, [2, 3, 5])}$$

A new tuple of two slots holding the first two references of t, so the list appears in both and it already had the 5 when the slice was taken.

Count the items

$$\texttt{len(t)} = 3,\ \texttt{len(v)} = 2$$

Top level items only. Neither length says anything about the three numbers inside the nested list.

Answer $$\boxed{\texttt{(1, [2, 3, 5], 4)}\;/\;\texttt{(1, [2, 3, 5])}\;/\;\texttt{3 2}}$$
Check

Check the sharing directly: print(v[1] is t[1]) prints True, so there is one list with two homes, and that is why editing it through u shows up in both printed lines.

3§06.5 — a grouping function that keeps one name

Exam level, and the program is somebody else's work. The function is supposed to hand back a dictionary from the word pass or fail to the list of names in that group. On the data below the right answer has three names in it, two under pass and one under fail.

def group_names(marks):
    """Assumes marks is a dictionary of name to number.
    Returns a dictionary of the word pass or fail to the list of names.
    """
    groups = {}
    for name in marks:
        if marks[name] >= 50:
            groups['pass'] = [name]
        else:
            groups['fail'] = [name]
        return groups

book = {'Ada': 91, 'Bora': 44, 'Cem': 72}
print(group_names(book))

Sample Run:

{'pass': ['Ada']}

The five steps the author would describe are numbered below. Exactly two of them are wrong.

Find(a) Which two steps are wrong?
Given
  • The input is {'Ada': 91, 'Bora': 44, 'Cem': 72}.

  • Step 1: start with an empty dictionary, groups = {}.

  • Step 2: walk the names with for name in marks:.

  • Step 3: choose the key with if marks[name] >= 50: and its else.

  • Step 4: store the name with groups['pass'] = [name] or groups['fail'] = [name].

  • Step 5: hand the dictionary back with a return groups written inside the loop body.

Hint 1/4

The output has one name in it out of three. Two separate things could cause that: names being written over each other, and the function leaving early.

Hint 2/4

A dictionary of lists needs two lines, one for the first name under a key and one for the rest. A return inside a loop body ends the function on the first pass.

Hint 3/4

Here the input is three names, Ada with 91, Bora with 44 and Cem with 72, and the printed answer is a dictionary with a single pass entry holding only Ada.

Hint 4/4

One of the wrong steps is the one that stores, and the other is the one that returns.

Show solution

Find the one that explains the missing two names

$$\texttt{return groups}\ \text{inside the}\ \texttt{for}\ \text{body}$$

A return ends the whole call, not the pass, so the loop runs once. This is the error that makes the printed answer one pair long.

Find the one that survives the first fix

$$\texttt{groups['pass'] = [name]}$$

A new list each time, so the second pass name replaces the first. With the return moved below the loop the output would still be wrong, which is how you know this is a second error and not a consequence of the first.

$$\texttt{if key in groups: groups[key].append(name)}$$

The fix. The first name under a key needs a list built round it and every later one is appended to the list already stored.

Clear the other three

$$\texttt{groups = \{\}}$$

Above the loop, so it survives the passes, and empty because nothing is known in advance about which groups will be needed.

$$\texttt{marks[name] >= 50}$$

Fifty passes, which the else branch then handles by exclusion. Nothing in the data or the question contradicts it.

Answer $$\boxed{\text{Steps 4 and 5}}$$
Check

Independent check on the counting: three names go in, so the lists in the answer must hold three names between them. The buggy version holds one and the fixed one holds two plus one, which is the arithmetic that catches this class of bug without reading any code.

4§06.5 — merging two stock dictionaries without touching either

Exam level, about fifteen marks. Write a function merge_stock(left, right) that takes two dictionaries from part name to an int count and returns a NEW dictionary holding every part that is in either of them, with the counts added together where a part is in both. Neither dictionary passed in may be changed. Then use it on the two dictionaries below and report how many different parts there are and the total count.

Find
  1. (a) Write the function with its docstring.

  2. (b) Write the four lines your script prints for the two dictionaries above.

Given
  • shelf is {'nut': 120, 'bolt': 45} and box is {'bolt': 30, 'washer': 8}.

  • bolt is the only part in both.

  • Neither input may be changed, and the answer must be a new dictionary.

Hint 1/4

The word NEW in the question is a requirement with a one line answer. Decide what the body starts from before deciding what the loops do.

Hint 2/4

Copy the first dictionary pair by pair into an empty one, then walk the second and either add to an existing count or store a new pair.

Hint 3/4

Here the first loop puts nut at 120 and bolt at 45 into the answer, and the second loop finds bolt already there, so 45 and 30 are added, and washer is new.

Hint 4/4

The merged dictionary has three parts, the bolt count is 75, and the total of the three counts is 203.

Show solution

Satisfy the word NEW first

$$\texttt{merged = \{\}}$$

An empty dictionary built inside the body. Anything else, including a copy made later, leaves a version of this function that shares an object with a caller.

$$\texttt{for part in left: merged[part] = left[part]}$$

Copying pair by pair. It reads left and writes only into the new dictionary, so left cannot be affected.

The second dictionary, two cases

$$\texttt{if part in merged: merged[part] = merged[part] + right[part]}$$

The part is already in the answer, so the counts are added. Reading from merged rather than from left keeps the line correct even if the same part appeared twice in a longer chain of merges.

$$\texttt{else: merged[part] = right[part]}$$

A part only in the second dictionary is simply stored, which is why washer arrives with 8.

Report on the answer

$$\texttt{len(both)} = 3$$

Pairs, so different parts: nut, bolt and washer.

$$\texttt{sum(both.values())} = 203$$

The values view can be summed directly. It is not a list and cannot be indexed, but it can be walked and added up.

Answer $$\boxed{\texttt{\{'nut': 120, 'bolt': 75, 'washer': 8\}},\ 3\ \text{parts},\ 203\ \text{in total}}$$
Check

Independent check on the total: the four input counts are 120, 45, 30 and 8, which add to 203, and merging can neither lose nor invent a count.

D · interleaved 3 questions
1§06.3 — a list whose own contents are the indices

A while loop walks a list, and the thing it assigns to is chosen by what the list currently holds.

a = [2, 4, 1, 0, 3]
i = 0
while i < len(a):
    a[a[i]] = i
    i = i + 1
print(a)
Find(a) Write the one line this prints.
Given
  • a starts as [2, 4, 1, 0, 3].

  • The line inside the loop reads a[a[i]] = i.

IPython console
Hint 1/4

Take the inner bracket first on every pass. Work out a[i] from the list as it stands now, and that number is the position being written to.

Hint 2/4

In a[a[i]] = i the right hand side is the loop counter and the position on the left is worked out from the current contents.

Hint 3/4

With a starting as [2, 4, 1, 0, 3], the first pass has i of 0 and a[0] of 2, so position 2 is set to 0.

Hint 4/4

Five passes, and two of them write to a position that has already been written to, so two of the five values end up the same.

Show solution

Pass by pass, rewriting the list each time

$$i=0:\ \texttt{a[0]}=2\Rightarrow\texttt{a[2] = 0}\Rightarrow\texttt{[2, 4, 0, 0, 3]}$$

The inner bracket is read from the list as it is now, and position 2 held 1 before this.

$$i=1:\ \texttt{a[1]}=4\Rightarrow\texttt{a[4] = 1}\Rightarrow\texttt{[2, 4, 0, 0, 1]}$$

Position 4 held 3, and now holds the counter.

$$i=2:\ \texttt{a[2]}=0\Rightarrow\texttt{a[0] = 2}\Rightarrow\texttt{[2, 4, 0, 0, 1]}$$

Position 2 was overwritten on the first pass, so the index read here is 0 rather than the original 1, and writing 2 into position 0 changes nothing because it was already 2.

$$i=3:\ \texttt{a[3]}=0\Rightarrow\texttt{a[0] = 3}\Rightarrow\texttt{[3, 4, 0, 0, 1]}$$

Position 0 is written a second time, and this is where the first value stops being 2.

$$i=4:\ \texttt{a[4]}=1\Rightarrow\texttt{a[1] = 4}\Rightarrow\texttt{[3, 4, 0, 0, 1]}$$

Position 4 was overwritten on the second pass, so the index read is 1, and position 1 already held 4.

Answer $$\boxed{\texttt{[3, 4, 0, 0, 1]}}$$
Check

Check the loop rather than the values: the test is i < len(a) and nothing in the body changes the length, so there are exactly five passes and the last index used is 4.

2§06.4 — two functions that shorten a list, one of them only locally

Two functions, each of which shortens a list by one item, and only one of them shortens the caller's.

def shrink(values):
    """Assumes values is a list of numbers.
    Removes the last item of the list it was given and returns nothing.
    """
    values.pop()

def shrink_copy(values):
    """Assumes values is a list of numbers.
    Returns a new list without the last item and leaves the original alone.
    """
    values = values[:len(values) - 1]
    return values

data = [1, 2, 3]
shrink(data)
print(data)
smaller = shrink_copy(data)
print(smaller, data)
Find(a) Write the two lines this prints.
Given
  • data starts as [1, 2, 3].

  • shrink calls pop on its parameter; shrink_copy assigns a slice to its parameter and returns it.

IPython console
Hint 1/4

For each body, ask whether it changes an object or points a local name at a new one. That single question decides both printed lines.

Hint 2/4

pop on a parameter changes the caller's list. Assigning a slice to the parameter name builds a new list and rebinds only the local name, so the caller's list is untouched.

Hint 3/4

Here shrink(data) pops the 3 from [1, 2, 3], and shrink_copy is then called on the two item list that is left.

Hint 4/4

The first printed line has two items; on the second line the returned list has one item and data still has two.

Show solution

The body that changes the object

$$\texttt{values.pop()}\Rightarrow\ \texttt{data} = \texttt{[1, 2]}$$

Pop with no argument removes the last item from the object the parameter refers to, which is the caller's list.

The body that rebinds

$$\texttt{values = values[:len(values) - 1]}$$

The slice builds a new list of the first item only, and the assignment points the local name at it.

$$\texttt{smaller} = \texttt{[1]},\ \texttt{data} = \texttt{[1, 2]}$$

The return carries the new list out, and the caller's list is exactly what the first call left behind.

Answer $$\boxed{\texttt{[1, 2]}\;/\;\texttt{[1] [1, 2]}}$$
Check

A check on the second call that does not involve the first: print(shrink_copy(data) is data) prints False, and printing data afterwards still shows two items.

3§06.5 — a file turned into a dictionary, then questioned

The program writes a small data file first so that it can run anywhere, then reads it back into a dictionary and asks two things about it.

out_file = open('stock.txt', 'w')
out_file.write('nut,120\n')
out_file.write('bolt,45\n')
out_file.write('washer,8\n')
out_file.close()

def read_stock(filename):
    """Assumes filename names a file whose lines are a part name and a count
    separated by a comma.
    Returns a dictionary of part name to int count.
    """
    stock = {}
    in_file = open(filename, 'r')
    for line in in_file:
        parts = line.strip().split(',')
        stock[parts[0]] = int(parts[1])
    in_file.close()
    return stock

table = read_stock('stock.txt')
print(table)
print('bolt count', table['bolt'])
low = []
for part in table:
    if table[part] < 50:
        low.append(part)
print('running low:', low)
Find(a) Write the three lines this prints.
Given
  • The file holds three lines: nut,120 then bolt,45 then washer,8.

  • Each line is stripped and split on the comma, and the second piece is turned into an int.

IPython console
Hint 1/4

Work out what one pass of the reading loop does to one line, then trust it for the other two and move on to the two questions at the end.

Hint 2/4

line.strip() removes the newline, split(',') gives a list of two strings, and int on the second one makes the value a number.

Hint 3/4

Here the three lines give the pairs nut 120, bolt 45 and washer 8, in that order, and the test at the end is a count below 50.

Hint 4/4

Three pairs printed in file order, one value fetched by key, and two of the three parts are below fifty.

Show solution

One line of the file becomes one pair

$$\texttt{strip()}\ \text{on the line}\;\rightarrow\;\texttt{'nut,120'}$$

The newline at the end of every line of a file has to go before the split, or the int conversion on the last piece would meet it.

$$\texttt{.split(',')}\;\rightarrow\;\texttt{['nut', '120']}$$

Two strings, and the second one is still text at this point.

$$\texttt{stock['nut'] = int('120')}$$

The key is the first piece and the value is the second turned into a number, which is what makes the comparison at the end possible.

The two questions

$$\texttt{table['bolt']} = 45$$

A lookup by key, which is the whole reason the data went into a dictionary rather than a list of pairs.

$$\texttt{low} = \texttt{['bolt', 'washer']}$$

45 and 8 are below fifty and 120 is not, and the order of the answer follows the order of the walk over the keys.

Answer $$\boxed{\texttt{\{'nut': 120, 'bolt': 45, 'washer': 8\}},\ 45,\ \texttt{['bolt', 'washer']}}$$
Check

Independent check on the counts: the three values add to 173, and sum(table.values()) would print exactly that. If any value were still a string the addition would fail instead, so the sum is also evidence that the conversion happened.

Mistake ledger (29 entries)
⚠ Writing a one item tuple without the comma

Brackets look like what makes a tuple, and in every other line of Python they group an expression.

wrong$$\texttt{t = (5)}\;\Rightarrow\;\texttt{len(t)}\;\text{is a TypeError}$$
right$$\texttt{t = (5,)}\;\Rightarrow\;\texttt{len(t)} = 1$$
⚠ Expecting a join to change the tuple it started from

t1 = t1 + t2 reads like an instruction to grow t1, and with lists there is an operation that really does that.

wrong$$\texttt{t1 + t2}\;\Rightarrow\;\texttt{t1}\;\text{is longer}$$
right$$\texttt{t1 + t2}\;\Rightarrow\;\text{a new tuple};\ \texttt{t1}\;\text{unchanged}$$
⚠ Assigning to an item of a tuple

Indexing works for reading, so it looks as though it will work for writing, which is true of lists and of nothing else here.

wrong$$\texttt{week[0] = 'sun'}$$
right$$\texttt{week = ('sun',) + week[1:]}$$
⚠ Expecting print to show the numbers of a range

Every other structured value prints its contents, so a range looks broken when it does not.

wrong$$\texttt{print(range(5))}\;\Rightarrow\;\texttt{[0, 1, 2, 3, 4]}$$
right$$\texttt{print(range(5))}\;\Rightarrow\;\texttt{range(0, 5)}$$
⚠ Joining or repeating ranges

Tuples and strings both allow + and *, and a range behaves like a tuple in every other respect.

wrong$$\texttt{range(3) + range(3, 6)}$$
right$$\texttt{tuple(range(3)) + tuple(range(3, 6))}$$
⚠ Reading the stop as the last value

The word stop sounds like where it ends up rather than where it gives up.

wrong$$\texttt{range(3, 30, 4)}\;\Rightarrow\;\text{last is}\ 30\ \text{or}\ 29$$
right$$\texttt{range(3, 30, 4)}\;\Rightarrow\;\text{last is}\ 27$$
⚠ Assigning from an operation of the first family

Every other language has a sort that hands the list back, and the line reads like English.

wrong$$\texttt{best = marks.sort()}$$
right$$\texttt{best = sorted(marks)}\quad\text{or}\quad\texttt{marks.sort()}$$
⚠ Using append where extend was meant

Both add things at the end, and with a single number as the argument they even look the same.

wrong$$\texttt{L1.append(L2)}\;\Rightarrow\;\texttt{[1, 2, 3, [4, 5]]}$$
right$$\texttt{L1.extend(L2)}\;\Rightarrow\;\texttt{[1, 2, 3, 4, 5]}$$
⚠ Calling index or remove without checking first

It works on every test where the item is present, which is every test you write while the code is fresh.

wrong$$\texttt{friends.index('Zeynep')}\;\Rightarrow\;\text{ValueError}$$
right$$\texttt{if 'Zeynep' in friends: friends.index('Zeynep')}$$
⚠ Reading pop as remove by value

The two words are close in meaning and both take one argument.

wrong$$\texttt{L.pop(48)}\;\Rightarrow\;\text{removes the item}\ 48$$
right$$\texttt{L.pop(48)}\;\Rightarrow\;\text{item at index}\ 48;\ \texttt{L.remove(48)}\ \text{by value}$$
⚠ Making a backup with a plain assignment

It is what a backup looks like, and for numbers and strings it even works, because nothing can change those.

wrong$$\texttt{backup = marks}$$
right$$\texttt{backup = marks[:]}\quad\text{or}\quad\texttt{list(marks)}$$
⚠ Expecting a clone to protect the lists inside it

The word copy sounds total, and for a list of numbers it is.

wrong$$\texttt{copy = rows[:]}\;\Rightarrow\;\texttt{copy[0]}\ \text{is safe}$$
right$$\texttt{copy[0]}\;\text{is}\;\texttt{rows[0]};\ \text{clone each row to separate them}$$
⚠ Using is to compare contents

It reads like the English word, and in a REPL two small equal ints can even give True.

wrong$$\texttt{if marks is [45, 60]:}$$
right$$\texttt{if marks == [45, 60]:}$$
⚠ Believing a tuple protects the list inside it

Immutable is taught as a property of the whole value rather than of its slots.

wrong$$\texttt{record[1].append(70)}\;\Rightarrow\;\text{TypeError}$$
right$$\texttt{record[1].append(70)}\;\Rightarrow\;\text{the inner list grows}$$
⚠ Rebinding a parameter and expecting the caller to see it

Changing an item through a parameter does reach the caller, so changing the parameter itself looks as though it should too.

wrong$$\texttt{values = [0, 0]}\;\text{inside a body}$$
right$$\texttt{values[0] = 0}\;\text{and}\;\texttt{values[1] = 0}$$
⚠ Giving a parameter an empty list as its default

It looks like the tidiest possible default and the bug only shows on the second call.

wrong$$\texttt{def collect(value, seen=[]):}$$
right$$\texttt{def collect(value, seen=None):}\;\text{then build it in the body}$$
⚠ Indexing a dictionary by position

The notation is the same as a list and the pairs are printed in a visible order, so the first one looks reachable.

wrong$$\texttt{phone[0]}\;\Rightarrow\;\text{the first pair}$$
right$$\texttt{phone[0]}\;\Rightarrow\;\text{KeyError: 0}$$
⚠ Reading a key before it exists

The counting pattern is written from the middle outwards, and the line that adds one is the interesting one.

wrong$$\texttt{counts[ch] = counts[ch] + 1}\;\text{on a new key}$$
right$$\texttt{if ch in counts: ... else: counts[ch] = 1}$$
⚠ Using a list as a key

A list of two things is the natural way to write a compound label, and the error message names a word the course never used.

wrong$$\texttt{scores[['a', 'b']] = 1}\;\Rightarrow\;\text{TypeError: unhashable}$$
right$$\texttt{scores[('a', 'b')] = 1}$$
⚠ Overwriting a list valued entry instead of appending to it

The line that creates the first entry is written first and then copied for the second case.

wrong$$\texttt{groups[letter] = [name]}\;\text{every time}$$
right$$\texttt{groups[letter].append(name)}\;\text{when the key is there}$$
⚠ Treating keys() as a list

It walks like a list and prints something that looks close to one.

wrong$$\texttt{d.keys()[0]}$$
right$$\texttt{sorted(d.keys())[0]}\quad\text{or}\quad\texttt{list(d.keys())[0]}$$
⚠ Calling the function instead of passing it

Every other use of a function name in the course has had brackets after it.

wrong$$\texttt{apply\_to\_each(L, abs())}$$
right$$\texttt{apply\_to\_each(L, abs)}$$
⚠ Assigning from a higher order function that changes the list

It looks like a mapping operation, and in other languages that is what it is.

wrong$$\texttt{L = apply\_to\_each(L, round)}\;\Rightarrow\;\texttt{None}$$
right$$\texttt{apply\_to\_each(L, round)}\;\text{on a line of its own}$$
⚠ Storing the result where the function was wanted

The dictionary of operations is usually written by copying a line that called one of them.

wrong$$\texttt{table = \{'add': add(3, 8)\}}$$
right$$\texttt{table = \{'add': add\}}$$
⚠ Passing a function's name as a string

The user types a word, so the word feels like the thing to pass on.

wrong$$\texttt{apply\_to\_each(L, 'abs')}$$
right$$\texttt{apply\_to\_each(L, abs)}\;\text{or look the string up in a dictionary first}$$
⚠ Removing from a list inside a for loop over it

It reads exactly like the sentence in the question, and on many inputs it gives the right answer.

wrong$$\texttt{for mark in marks: if mark < 50: marks.remove(mark)}$$
right$$\texttt{for mark in marks[:]: if mark < 50: marks.remove(mark)}$$
⚠ Advancing the counter after a removal in a while loop

Every other while loop in the course advances its counter on every pass, so the else looks unnecessary.

wrong$$\texttt{if marks[i] < 50: marks.pop(i)}\ \text{then}\ \texttt{i = i + 1}$$
right$$\texttt{else: i = i + 1}\ \text{so the counter waits when something was removed}$$
⚠ Removing pairs from a dictionary while walking it

The list version of the same loop runs without complaining.

wrong$$\texttt{for part in stock: stock.pop(part)}$$
right$$\texttt{for part in sorted(stock.keys()): stock.pop(part)}$$
⚠ Walking by index over a shrinking list

range(len(L)) is the standard walk, and it is worked out once before the loop starts.

wrong$$\texttt{for i in range(len(marks)): marks.pop(i)}\;\Rightarrow\;\text{IndexError}$$
right$$\texttt{while i < len(marks):}\ \text{with the counter advanced by hand}$$
Formula card
A tuple, and what immutable promises
$$\boxed{\texttt{t = (v}_0\texttt{, v}_1\texttt{, ..., v}_{n-1}\texttt{)}\quad\text{indices }0\ \text{to}\ n-1,\ \text{no item may be replaced}}$$

The commas build it, so a one item tuple is (5,). Slots cannot be replaced and the length cannot change; the objects in the slots are not protected.

A range, and when two of them are equal
$$\boxed{\texttt{range(a, b, c)} \equiv a,\ a+c,\ a+2c,\ \dots\ \text{while still short of}\ b}$$

Stops before the stop value. No + and no *. Indexing gives an int, slicing gives a range, and == compares the sequences rather than the arguments.

The two families of list operation
$$\boxed{\text{changes }L\Rightarrow\texttt{None}\ \text{back};\quad \text{hands a value back}\Rightarrow L\ \text{untouched};\quad \texttt{pop}\ \text{does both}}$$

Operations that change the list hand back None; operations that hand back a value leave the list alone. pop does both.

What a name holds, and how to get a second object
$$\boxed{\texttt{b = a}\Rightarrow\ \text{one object};\quad \texttt{b = a[:]}\Rightarrow\ \text{two objects};\quad \texttt{is}\ \text{asks which},\ \texttt{==}\ \text{asks what}}$$

Assignment copies a reference. L[:] and list(L) copy one level. is asks which object, == asks what contents.

A dictionary, and the one way in
$$\boxed{\texttt{d[k]}\ \text{finds the value stored under}\ k;\quad \text{keys unique and immutable, values anything}}$$

Keys unique and immutable, values anything. A missing key is a KeyError; in and get are the safe ways to ask. No access by position.

A function name against a function call
$$\boxed{\texttt{f}\ \text{is the function};\quad \texttt{f(x)}\ \text{is its result};\quad \text{a parameter can hold either}}$$

Without brackets it is the function object and can be stored, passed and defaulted to. With brackets it runs and stands for what it handed back.

Removing items while walking a structure
$$\boxed{\text{remove inside}\ \texttt{for}\ \text{over the same list}\Rightarrow\ \text{items skipped};\quad \text{clone, or}\ \texttt{while},\ \text{or build new}}$$

A for loop over a list counts positions and does not notice a change of length. A dictionary raises a RuntimeError instead of skipping.

Cloning a list
$$\texttt{copy = L[:]}\quad\text{or}\quad\texttt{copy = list(L)}$$

One level deep. Items that are themselves lists are shared by both copies.

Counting things into a dictionary
$$\texttt{if k in d: d[k] = d[k] + 1}\ \text{else}\ \texttt{d[k] = 1}$$

The key must exist before it can be read, which is what the test is for.

A dictionary whose values are lists
$$\texttt{if k in d: d[k].append(x)}\ \text{else}\ \texttt{d[k] = [x]}$$

The first item under a key needs a list built round it; every later one is appended to the list already stored.

Removing from a list while walking it
$$\texttt{i = 0}\;/\;\texttt{while i < len(L)}\;/\;\texttt{if ...: L.pop(i)}\ \text{else}\ \texttt{i = i + 1}$$

The counter advances only when nothing was removed. while i < len(L) reads the length on every pass, which range(len(L)) does not.

Walking a structure by position or by item
$$\texttt{for i in range(len(L))}\quad\text{against}\quad\texttt{for x in L}$$

Use the index form when the answer is a position, when items are being replaced, or when neighbours are compared. Use the item form otherwise.

Check yourself

Close the page and write, from memory: the four containers of this section, with one line each saying what you look a value up by and whether it can be changed; then the two families of list operation with three members each and what each family hands back; then the two lines of code that make a backup that survives a sort; then the two line pattern for counting things into a dictionary and the two line pattern for a dictionary whose values are lists; then what f means without brackets and what it means with them; and finally the three safe ways to remove items from a list you are walking. Then open the page and mark what you missed rather than reading it all again.

  • Write a function that hands back two numbers at once, and say what (5) is and how it differs from (5,)?

    c-tuple

  • Say how many values range(3, 30, 4) has, what its last value is, and why range(0, 7, 2) equals range(0, 8, 2) but not range(6, -1, -2)?

    c-range

  • Put six list operations into the right family from memory, and say what L = L.sort() leaves in L and what L.pop(2) leaves and hands back?

    c-list

  • Draw the names and objects for b = a and for c = a[:], and predict all three lists after one append through each name?

    c-alias

  • Build a dictionary from a name to a list of tuples, reach one mark through all three layers, and say what d[0] does to a dictionary whose keys are strings?

    c-dict

  • Write a function that takes another function and applies it to every item of a list, and say what goes wrong when the argument is written with brackets?

    c-higher-order

  • Take a for loop that removes items from the list it walks, say which item it skips on a given input, and rewrite it in all three safe ways?

    c-walk-and-change

Glossary (27 terms)
structured typeyapılandırılmış tür

A type whose values have parts you can reach, such as a string, a tuple, a range, a list or a dictionary, as against a number, which has none.

tupledemet

An ordered group of values written with round brackets and commas. It cannot be changed after it is built, and it may hold values of different types.

range

An immutable sequence of whole numbers described by a start, a stop and a step. It works out each value as it is asked for rather than storing them.

listliste

An ordered group of values written with square brackets whose items can be replaced, added and removed after it is built.

dictionarysözlük

A group of key and value pairs in which a value is found by giving its key. Keys are unique and must be immutable; values may be anything.

keyanahtar

The label a dictionary stores a value under, and the only way to reach that value. It is not a position.

mutabledeğiştirilebilir

Said of a type whose values can be changed in place. Lists and dictionaries are; numbers, strings, tuples and ranges are not.

immutabledeğiştirilemez

A value that cannot be altered once it exists: no slot can be replaced and the length cannot change. It says nothing about the objects those slots point at, so a tuple can hold a list that grows.

referencereferans

What a name actually holds when it names a structured object: a way of reaching that object rather than a copy of it.

aliastakma ad

A second name for one object, made by assigning one name from another. A change through either name is visible through both.

side effectyan etki

Anything a call does besides handing a value back, such as printing or changing a list the caller still has its own name for.

cloningkopyalama

Building a second list with the same items, written L[:] or list(L), so that the two lists can then be changed independently.

A copy that duplicates the row of references and not the objects they point at, which is what cloning a list gives you.

The operator is, which asks whether two names reach one and the same object, as against ==, which asks whether two values have the same contents.

indexingindeksleme

Reaching one item of a sequence by its position, counting from 0, or one value of a dictionary by its key.

slicingdilimleme

Taking a run of items from a sequence with s[a:b], which stops before b and always builds a new object.

The question x in s, which is True when x is one of the items of the sequence s, or one of the keys when s is a dictionary.

higher order function

A function that takes another function as an argument or hands one back, so that the work to be done is chosen by the caller.

function object

What a function name without brackets stands for: a value that can be stored, passed and defaulted to, and that runs only when brackets are put after it.

varsayılan değer

A value written in a header with an equals sign, used when a caller passes nothing for that parameter. It is worked out once, when the definition is read.

in placeyerinde

Said of an operation that changes the object it was given rather than building a new one. Such operations hand back None.

KeyError

The error that stops a program when a dictionary is asked for a key it does not have. Avoided with in or with get and a fallback.

IndexError

The error that stops a program when a sequence is asked for a position it does not have, which is every position from its length upwards.

ValueError

The error that stops a program when remove or index is asked for an item the list does not contain.

unhashable type

The message in the error raised when something that can change, such as a list, is used as a dictionary key.

split

A string method that cuts a string into a list of pieces, on a given separator or on whitespace when none is given.

view

What keys() and values() hand back: something that can be walked and summed but not indexed, and that follows the dictionary as it changes.

What comes next
§07 · Arrays and Multi-Dimensional Arrays

Everything on this page had one bracket after a name. The next section adds the second one. A table is a list whose items are lists, so a row is table[r] and a cell is table[r][c], and walking all of it takes two nested loops rather than one. Nothing new is needed for that: the aliasing rules are the ones from this page, and the reason a cloned table still shares its rows is the reason a cloned list still shares its inner lists.

Sources
  • kitapJohn Guttag, Introduction to Computation and Programming Using Python, with Application to Understanding Data, second edition, chapter 5 The syllabus names this chapter for the week. The chapter also covers list comprehensions and the higher order functions map and filter, which the lecture for this week does not use and which therefore appear on this page only as names.
  • ders malzemesiThe course's own lecture slides for this week Used for the boundary of what counts as covered: scalar against structured objects, tuples, ranges and range equality, the list method table, splitting strings, dictionaries with their function table, the memory drawings for aliasing and cloning, lists as function parameters, higher order functions, and tables of two dimensions, which the syllabus gives to the following week.
  • ders malzemesiThe lab sheet and the tutorial sheet for the tuples, lists and dictionaries lab Used for the shape of an exercise, which is a named script, stated function names with what they return, and a sample run given character for character, and for the instruction that only functionality covered in the course may be used and that functions need docstrings.
  • ders malzemesiOne past midterm paper for this course, with its solutions Used for the weight and the shape of the tracing question, which was worth 30 of the 100 marks and consisted of four short programs, two of whose parts were on this week's material: a list indexed by its own contents inside a while loop, and a tuple holding a list that was extended through it.
  • ders malzemesiThe course information page for one autumn term Used for the assessment weights, which are labs 20 per cent, midterm 40 and final 40, and for the fact that the exam paper prints a list of the available methods on its cover, which is why this page teaches behaviour rather than names.
  • sabitThe Python 3 language reference and its library documentation Used to check the exact wording of the error messages shown on this page, the rule that a default value is worked out once when the definition is read, the rule that a dictionary keeps the insertion order of its keys, and the behaviour of `sorted`, `sum`, `abs`, `bool`, `int` and `round` on the values used in the examples.

Spotted something missing or wrong? tell us · share your own notes or an old exam.

Last updated .