← back to CS 115
Week 2Guttag §2242 min full read
7 concepts20 worked examples29 exercises4 exam-level7 figures
What are you here for?

02 The Basic elements of Python, Branching, Strings, Input, Iteration (Chapter 2)

Start with this

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

§02.0 — comparing values that look like numbers

Three comparisons are printed, one per line. Two of them compare pieces of text and one compares numbers. Nothing is typed in while the program runs.

print('9' > '80')
print(9 > 80)
print('Ankara' > 'Adana')
Find(a) Write the three lines the program prints, in order.
Given
  • The characters being compared are the ones written in the program: the quotes are part of the program, not part of the value.

  • Text is compared character by character, left to right, the way a dictionary orders words.

IPython console
Hint 1/4

You are not being asked which value is bigger. You are being asked what each comparison hands back, and the first thing to settle for each line is whether the two sides are text or numbers.

Hint 2/4

Quoted characters are text and text is ordered like a dictionary: compare position 0, and only if those match move to position 1. Unquoted digits are numbers and are compared by size.

Hint 3/4

Line 1 compares the text '9' with the text '80', so only position 0 matters: '9' against '8'. Line 2 compares the numbers 9 and 80. Line 3 compares 'Ankara' with 'Adana', which agree up to position 1.

Hint 4/4

The three printed lines are True, then False, then True.

Show solution

Decide what kind of thing each side is

$$\texttt{'9' > '80'}$$

Both sides are in quotes, so both are text and the comparison is character by character, not by size.

$$\texttt{9 > 80}$$

No quotes, so these are numbers and the comparison is the ordinary one.

$$\texttt{'Ankara' > 'Adana'}$$

Text again, so again character by character.

Compare the first characters that differ

$$\texttt{'9' vs '8'}$$

Position 0 already differs, so the answer is fixed there: 9 comes after 8 in the character order, so the result is True.

$$\texttt{9 vs 80}$$

As numbers 9 is smaller, so this one is False, which is the opposite of the line above it.

$$\texttt{Ank vs Ada}$$

The first two characters match, so the decision falls to position 2, where k comes after d: True.

Answer $$\boxed{\texttt{True},\;\texttt{False},\;\texttt{True}}$$
Check

Check the middle line against ordinary arithmetic: 9 is not bigger than 80, so False is right there, and the fact that the first line disagrees is the whole point.

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

Someone hands in a program that asks for two numbers and prints the larger one. Typed 9 and then 80, it answers 9. Nothing is underlined in the editor, no error message appears, the program looks finished, and the mark for that question is gone.

By the end of this section you can take any short program built from this week's material and write down what it prints, character by character, blank lines included; and you can write, from a blank file, the kind of program the first two lab sessions ask for.

In 60 seconds

This week adds four moves to a script that used to run straight through: read a value in from a person, ask a question about it, take one character or one piece out of a piece of text, and do the same thing again until something changes. Each of the four has one detail that decides the mark.

Whatever is typed arrives as text
$$\texttt{input(prompt)}\;\longrightarrow\;\texttt{str}$$

Every time a number comes from the keyboard. The to int or float is not a tidying step afterwards, it is the second half of reading the number.

A slice keeps the start and drops the stop
$$\texttt{s[a:b]}\;=\;\texttt{s[a]},\ \texttt{s[a+1]},\ \dots,\ \texttt{s[b-1]}$$

Any time you cut a piece out of text. The length of what you get is b minus a, which is the fastest way to check a slice without writing the letters out.

One chain, one block
$$\texttt{if}\;/\;\texttt{elif}\;/\;\texttt{else}\;\Longrightarrow\;\text{exactly one block runs}$$

When the cases are alternatives. Two separate if statements are not the same shape: both of them can run, and that is usually the bug.

A range never reaches its stop
$$\texttt{range(a,b,c)}\;\Longrightarrow\;a,\;a+c,\;a+2c,\;\dots\;\text{while still}\;<b$$

Every counted loop. To walk a whole string the call is range(len(s)), because the last valid index is len(s) minus one.

Three most common mistakes
  1. Comparing two things the user typed without casting them. The test '9' > '80' is True, because text is compared character by character and 9 beats 8 on the first one.

  2. Calling a string method and throwing away what it hands back. A line that is just s.strip() changes nothing at all, because s still points at the old characters.

  3. Writing range(1, len(s)) when you meant every position. It starts one too late and stops one too early, so a single call gets both ends wrong and the program still runs.

Labs are 20 percent of the course mark, the midterm 40 and the final 40. On the midterm paper checked while writing this, one question was nothing but short programs to be traced by hand and it carried 30 of the 100 marks; the other questions on that paper each asked for a function, a dictionary or a file, which are all later sections. So this section is the one that makes a whole exam question answerable on its own. The two lab sessions it prepares you for are the one on data, expressions and conditional statements and the one on strings and loops.

How much time do you have?
10 minutes

The one fact that breaks most first programs, namely that hands back text, plus the two index rows of a string. With these you can answer a tracing part that only walks a string, and you will stop writing the comparison bug in the opening story.

The 60-second card · Reading a value that a person types · Taking one character, and taking a piece · Formula card
45 minutes

Everything that turns into a program: reading and casting, cutting text up, choosing one branch out of several, and the two loop shapes with the three parts that make a loop stop. This is the whole of both lab sessions and most of a tracing question.

The 60-second card · Reading a value that a person types · Taking one character, and taking a piece · Choosing a path, and the indentation that decides which · Repeating while a question is still true · Counting loops: for and range · Scaffolding comes off · B · computation
full read

Adds the parts that separate a working program from a correct one: what each string operation returns and what it never changes, loops inside loops, break, and the traps that this course actually sets on paper.

The 60-second card · Recall first · Conventions · Reading a value that a person types · Taking one character, and taking a piece · What a string operation hands back, and what it never changes · Choosing a path, and the indentation that decides which · Repeating while a question is still true · Counting loops: for and range · A loop inside a loop, and the two ways out · 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. Read a value from the keyboard and convert it to the type the rest of the program needs, and say what goes wrong when the conversion is left out.

  2. Compute the value of any index or slice expression on a given string, including negative indices and a negative step, and say which of them raise an error instead.

  3. State for each string operation in this section what it hands back, what it leaves unchanged, and what it does when the string is empty.

  4. Write a chain of tests that sorts a value into one of several bands, order the tests so that no band is unreachable, and predict the output of a chain that is in the wrong order.

  5. Build a loop that keeps reading until a stop value arrives, with the three parts in the right places, and show why a missing or misplaced third part never ends.

  6. Predict exactly which values a range call produces, and pick between a counted loop and a conditional loop for a given task.

  7. Trace a loop inside a loop, including the case where the bound is reassigned in the body, and say which loop a break leaves.

Syllabus coverage

The Basic elements of Python — covered

The rest of the chapter's basic material, which is the part the previous section did not need: comparing values with the , combining tests with and, or and not, operator precedence when a comparison meets arithmetic, and what counts as true when the thing tested is not a comparison at all.

Objects, types, variables, arithmetic and output formatting are the first half of the same chapter and were covered in the previous section; this page recalls them where it uses them rather than repeating them.

Branching — covered

  • The if statement
  • the if and else pair
  • the elif chain
  • nesting
  • the role indentation plays in deciding which statements belong to which test

Strings — covered

Text as a sequence of characters

  • length
  • indexing from both ends
  • slicing with a step
  • joining and repeating
  • membership with in
  • comparison order
  • the operations that hand back a new string rather than editing the old one

Split is the one operation on the exam cover sheet that is left out here on purpose: it hands back a list, and lists are a later section, so nothing on this page needs it.

Input — covered

Reading a value typed at the keyboard, the type that comes back, converting it, and what the program does when the characters cannot be converted.

Iteration — covered

Repetition with while and with for: the three parts of a loop that ends, what a range call produces, walking a string either by character or by index, loops inside loops, and leaving a loop early with break.

Chapter 2 — covered

The textbook chapter behind the whole line, whose second half is exactly this material.

The chapter also opens the topic of numerical programs built on these loops, such as guess and check and bisection search; the syllabus gives those their own line in the next section, so they are deferred rather than squeezed in here.

Recall first
Converting text to a number and back

int('7') hands back the whole number 7, float('7.5') hands back 7.5, and str(7) hands back the two characters that spell it. int refuses anything that is not a whole number written out, so int('7.5') stops the program with a , while int(float('7.5')) gives 7 by cutting the fraction off.

Everything typed at the keyboard is text, so on this page almost every number passes through one of these three calls before it is used.

Floor division and remainder

a // b is the division with the fraction thrown away and a % b is what is left over, so 17 // 5 is 3 and 17 % 5 is 2, while 17 / 5 is 3.4 and is a float even when it comes out whole.

The remainder is how a test asks is this divisible, and floor division is how a loop peels the digits off a number one at a time.

What print does with its arguments

print writes its arguments with exactly one space between neighbours and then ends the line, so print('Sum:', 20) writes Sum: 20. Joining with + instead puts nothing between the pieces, and print(x, end='') leaves the line unfinished so the next print continues on it.

Half the marks on a tracing question are spaces and line breaks, and end='' is how the loops here put several values on one line.

Assignment reads the right side first

count = count + 1 works out the right side with the old value and only then moves the name to the answer; count += 1 is the same thing written shorter. A name that has never been assigned cannot be read, which is why counters are set up before the loop.

Every loop in this section changes a value it has just read, and the order of those two halves is what makes the loop end.

Formatting a number to a fixed number of decimals

format(total, '.2f') hands back the text of total rounded to two decimal places, so format(1349.892, '.2f') is 1349.89. The unrounded value is still there in the variable; the formatting only changes the characters that get printed.

The lab papers ask for a given number of decimal places in the printed line while also asking for the exact value, and those are two different things.

An error that stops the program against one that does not

A syntax error is refused before a single line runs, so nothing at all is printed. A program that is accepted can still be wrong: it runs, prints something, and nothing complains. The second kind is the one that costs marks.

The opening story on this page is the second kind, and so are most of the traps in this section.

Try it yourself first (2 questions)
1§02.0 — the three division operators

A program prints five division results on two lines. Everything in it is a whole number written into the program itself.

print(17 // 5, 17 % 5, 17 / 5)
print(-17 // 5, -17 % 5)
Find
  1. (a) Write the two lines the program prints.

  2. (b) Say in one sentence which of the five results is not a whole number, and why.

Given
  • The three operators are /, // and %.

  • The second line uses a negative left-hand side, which is where the two floor-style operators stop agreeing with a hand calculation.

IPython console
Hint 1/4

Nothing here needs a calculator. What it needs is a decision for each of the three operators about what it keeps and what it throws away.

Hint 2/4

a / b keeps the fraction and gives a float; a // b goes to the whole value below; a % b is whatever must be added to (a // b) * b to get back to a.

Hint 3/4

With a = 17 and b = 5 the three are 3, 2 and 3.4. With a = -17 the first is the value below -3.4, which is -4.

Hint 4/4

The two lines are 3 2 3.4 and -4 3.

Show solution

Take the positive line first

$$\texttt{17 // 5 = 3}$$

Five goes into seventeen three whole times, and the fraction is dropped rather than rounded.

$$\texttt{17 \% 5 = 2}$$

What is left after those three fives: 17 minus 15.

$$\texttt{17 / 5 = 3.4}$$

The one operator that keeps the fraction, and it hands back a float even when the division comes out exact.

Now the negative line, where the rule bites

$$\texttt{-17 // 5 = -4}$$

Floor division goes to the value below, and -3.4 sits between -4 and -3, so the answer is -4, not -3.

$$\texttt{-17 \% 5 = 3}$$

The two must fit together: -4 times 5 is -20, and -20 plus 3 is -17, so the remainder is 3 and it is positive.

Answer $$\boxed{\texttt{3 2 3.4}\quad\text{and}\quad\texttt{-4 3}}$$
Check

Multiply back: for any pair, (a // b) * b + a % b must return a. Here -4 times 5 plus 3 is -17, so the negative pair is consistent.

2§02.0 — spaces that print puts in without being asked

Three print calls carry the same three pieces of information but write different characters to the screen.

print('Total:', 3, 'items')
print('Total: ' + str(3) + ' items')
print('Total:', 3, 'items', sep='')
Find(a) Write the three printed lines exactly, and mark where a space appears that nobody typed.
Given
  • The three calls differ only in how the pieces are joined: commas, then + with a conversion, then commas with sep=''.

  • A comma between two arguments of print is not a character that gets printed; it is a separator that print replaces with one space.

IPython console
Hint 1/4

The question is not what the program means but which characters reach the screen, so the thing to track is how many separate arguments each print call is given.

Hint 2/4

Between neighbouring arguments print writes its separator, which is one space unless sep= says otherwise. Text joined with + arrives as a single argument, so no separator is inserted at all.

Hint 3/4

Call 1 has three arguments and the default separator; call 2 has one argument; call 3 has three arguments and an empty separator.

Hint 4/4

The lines are Total: 3 items, Total: 3 items and Total:3items.

Show solution

Count what each call hands to print

$$\texttt{print(a, b, c)}$$

Three arguments, so print inserts its separator twice: one space after Total: and one after the 3.

$$\texttt{print(a + str(b) + c)}$$

One single argument, already joined, so print inserts nothing; the spaces you see were typed inside the quotes.

$$\texttt{sep=''}$$

Three arguments again, but the separator is now the empty text, so the pieces are written with nothing between them.

Answer $$\boxed{\texttt{Total: 3 items}\;/\;\texttt{Total: 3 items}\;/\;\texttt{Total:3items}}$$
Check

Count characters rather than trusting the look: lines 1 and 2 are 14 characters, line 3 is 12, and the missing 2 are exactly the two separators.

Notation
symbolreads asmeanswatch out
$\texttt{s[i]}$

s at i, or the character at i

The single character sitting at position i, counted from 0 at the left end. It is always a string of length 1.

i must be at most len(s) minus 1. One past the end is not an empty answer, it is an that stops the program.

$\texttt{s[-i]}$

s at minus i

The character counted from the right end, where -1 is the last one, so s[-1] and s[len(s)-1] are the same character.

There is no -0. The left end is 0 from one side and -len(s) from the other.

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

s from a up to b

A new string with the characters at a, a+1 and so on, stopping before b. Its length is b minus a.

b is never included. Leaving a out means from the start, leaving b out means to the end, and a slice that runs past the end is trimmed silently instead of failing.

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

s from a up to b in steps of c

The same, but taking every c-th character. A negative c walks leftwards, which is how s[::-1] reverses a string.

With a negative step the roles of a and b flip: a must be to the right of b or the result is empty.

$\texttt{==}$

is equal to

A question, whose answer is True or False.

Not the same as =, which is a command that moves a name. Writing = inside an if is refused as a syntax error, which at least means you find out immediately.

$\texttt{!=}$

is not equal to

True exactly when == would be False.

With text it is case sensitive: 'Ali' != 'ali' is True.

$\texttt{in}$

occurs inside

True when the left string appears somewhere inside the right one, as a run of neighbouring characters.

Case sensitive, and it asks about a run, so 'ak' in 'ankara' is False even though both letters are there.

$\texttt{and},\ \texttt{or},\ \texttt{not}$

and, or, not

Join two questions, or flip one. and needs both sides, or needs at least one, not swaps the answer.

not binds tighter than and, and and binds tighter than or, so brackets are the only way to be sure a reader and Python agree.

$\texttt{range(a,b,c)}$

range from a up to b in steps of c

The values a, a+c, a+2c and so on, for as long as they stay on the near side of b.

b is excluded, exactly like a slice. With one argument the start is 0; with two the step is 1.

$\texttt{end=}$

end equals

What print writes after the last argument instead of ending the line. end='' writes nothing, so the line stays open.

After a loop that used end='' the line is still unfinished; a bare print() is what closes it.

$\text{four spaces}$

indentation

Which statements belong to the if or the loop above them. In Python this is not layout, it is the program.

Moving one line in or out by four spaces changes what the program does without changing a single word of it.

Conventions used here
Every output on this page was produced by running the code.

No output here was predicted by eye. Each program was run and the characters it wrote were copied into the page, which is why a few of them are uglier than a textbook would print: a float that prints as 1349.8920000000003, a line that ends in a space because a loop used end=' ', a run that stops with an error message. Where a program cannot finish, the error type is shown as the last line of its output.

The whole value of this chapter is that the output is exactly right, so a page about it cannot itself be guessing.

Blank lines and trailing spaces are part of the answer.

When a question here asks for output, the answer is the characters, not a description of them. A print() with nothing in it contributes an empty line and that empty line counts. A loop body that ends with print(v, end=' ') leaves a space after the last value and that space counts too. Where a blank or a line break is doing real work, the surrounding text says so in words as well, because a space is invisible in a code block.

On paper the marker is comparing your characters with theirs, so prints the numbers separated by commas is not an answer.

Indentation here is four spaces, and it is never decorative.

Every indented block on this page is indented by four spaces, which is what the course's own style guide asks for. Any consistent amount would run, but mixing amounts inside one block is refused, and a tab is not four spaces even when it looks like them. When a line is moved in or out to make a point, the text says which block it now belongs to rather than leaving you to count.

Two of the worked examples on this page differ from a broken version only by indentation, so the amount has to be fixed and visible.

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, elif, else, while, for, range, break, and the string operations listed in the notation table. Writing your own functions, lists, dictionaries and files all arrive in later sections and none of them is used here, which is also why no solution on this page defines anything with def. The closed-book exam cover sheet lists methods from those later chapters as well; they belong to the sections that teach them.

A solution that reaches for a tool you have not been given is worthless in a lab and is the fastest way to waste an evening.

A loop condition never tests a float for equality.

Counters that control a loop are whole numbers here. Adding 0.1 ten times does not land on 1.0, so a condition like while x != 1.0 can step straight past its target and run on; the run in the loop section shows it doing exactly that. Where a float has to be compared, this page compares with < or > on a quantity that is allowed to overshoot, such as a balance passing a target.

An from a float comparison looks like a hung computer rather than a wrong answer, so it is worth ruling out by habit.

Two spellings of the same formatted number.

Where a printed line needs a fixed number of decimals, the solutions here use format(value, '.2f'), because that is the spelling the closed-book cover sheet lists. The lecture slides write the same thing as an f-string with the field after the colon, and the two produce the same characters. The part worth remembering is the field itself, .2f, which is identical in both spellings.

It is worth knowing before the exam that the tool you practise with and the tool on the cover sheet can be spelled differently while the part you have to recall does not change.

2.1Reading a value that a person types

Reading a number is two steps, not one: collect the characters, then turn them into a number.

Every script so far carried its data inside the file. Once the data comes from a person, one step appears in front of all the arithmetic.

Solvable with what we have
  • Work out an area from a width and height written into the file.

  • Turn 3.9 into 3 with int, and 3 into 3.0 with float.

  • Print a total to two decimals and the exact value too.

Not solvable yet
  • Ask the person running the program for the width.

  • Say which of two typed numbers is the larger one.

  • Keep asking for values until a stop value is typed.

The program from the opening, written the obvious way.

first = input('Enter the first number: ')
second = input('Enter the second number: ')
if first > second:
    print(first, 'is the larger one')
else:
    print(second, 'is the larger one')

Sample Run:

Enter the first number: 9
Enter the second number: 80
9 is the larger one
Why it fails

Nothing is broken enough to complain. Two pieces of text can legally be compared, so the program answers a question nobody asked: not which number is bigger, but which text comes later in the character order. That comparison stops at the first position where the two differ, sees 9 against 8, and says True.

RuleRule 2.1: input always hands back text
Conditions
  • The , if you give one, is written with no line break of its own, so the typed characters appear on the same line. That is why prompts normally end with a space.

  • What comes back is a str, whatever the characters look like. There is no automatic conversion and no warning.

  • The Enter key ends the reading and is not part of the value, so the text has no line break on the end of it.

$$\boxed{\texttt{value = input(prompt)}\;\Longrightarrow\;\texttt{type(value)}\;\text{is}\;\texttt{str}}$$

Whatever the person types, the name ends up pointing at a piece of text. If the program is going to do arithmetic with it, the conversion has to be written down, either around the input call or on the next line.

Looks like this, but is not

This looks like a program that triples the hours worked.

answer = input('How many hours did you work? ')
print(answer)
print(type(answer))
print(answer * 3)

It triples the characters instead: multiplying text by a whole number repeats it, so 7 becomes 777 and nothing complains. Adding one call turns the same four lines into arithmetic.

Sample Run:

How many hours did you work? 7
7
<class 'str'>
777

Sample Run:

How many hours did you work? 7
7
<class 'int'>
21
what was typedint of itfloat of it

41

41

41.0

41.5

ValueError

41.5

-7

-7

-7.0

0

0

0.0

8 with a space on each side

8

8.0

7 hours

ValueError

ValueError

nothing, Enter on its own

ValueError

ValueError

Two things are worth taking from this. First, int is the fussier of the two: it refuses text that spells a decimal, so a program that might be given 41.5 must read it with float, and int(float(text)) is the way to get a whole number out of it. Second, surrounding spaces are forgiven but anything else is not, and the failure is loud: the program stops on that line.

The larger of two typed numbers, fixed

Repair the opening program so that it compares the numbers rather than the characters, and check it on a pair where the two readings disagree.

FindA program that prints the larger of the two values and the wording of the check that proves it is now comparing numbers.
Given
  • The two values come from the keyboard and are whole numbers.

  • The pair 9 and 80 is the one that exposed the bug.

Solution

Decide where the conversion goes

$$\texttt{first = int(input(...))}$$

Around the input call, not later: if the conversion is put off, there are two names for the same thing and it is easy to compare the wrong one.

$$\texttt{second = int(input(...))}$$

The same for the second value, for the same reason.

Leave the comparison exactly as it was

$$\texttt{if first > second:}$$

The comparison was never the problem. Now that both sides are numbers, this same line asks the intended question.

Read the run

$$\texttt{9 > 80}\;\text{is}\;\texttt{False}$$

So the else branch runs and 80 is printed, which is the opposite of what the broken version said.

$$\texttt{print(second, 'is the larger one')}$$

Two arguments, so print puts one space between them: the line is 80, a space, then the message.

Answer $$\boxed{\texttt{80 is the larger one}}$$
Check

Run it with 80 first and 9 second. A program that compares numbers must give the same answer both ways round, and the broken one does not: as text, '80' > '9' is False.

The repair was one call in two places, and the bug it removed was invisible. That is the shape of most of the marks lost on this chapter: legal code answering the wrong question.

A price, a tax rate, and a line with two decimals

Write a program that reads a net price and a tax rate as a percentage, then prints the total both rounded to two decimals and exactly as the machine holds it.

price = float(input('Net price (TL): '))
rate = float(input('VAT rate (percent): '))
total = price * (1 + rate / 100)
print('Total is ' + format(total, '.2f') + 'TL')
print('Exact total is', total)

Sample Run:

Net price (TL): 1249.9
VAT rate (percent): 8
Total is 1349.89TL
Exact total is 1349.8920000000003
FindThe two printed lines, and why they disagree in the third decimal.
Given
  • The price and the rate are typed by the user and may have decimals.

  • The rounded line wants two decimal places; the second line wants whatever the machine actually has.

Solution

Pick the conversion that cannot refuse the input

$$\texttt{float(input(...))}$$

A price may well be typed as 1249.9, and int would stop the program on that text, so float is the only safe choice here.

Do the arithmetic once, then format twice

$$\texttt{total = price * (1 + rate / 100)}$$

8 per cent is 0.08 as a fraction, so the multiplier is 1.08; dividing by 100 inside the expression keeps the reading as a percentage where the user typed it.

$$\texttt{format(total, '.2f')}$$

Formatting changes the characters that get printed and leaves total alone, which is why the next line can still show the exact value.

$$\texttt{print('Total is ' + ... + 'TL')}$$

Joining with + is used here rather than commas because the unit has to touch the number with no space: the commas would insert one.

Read the two lines against each other

$$\texttt{1349.89}$$

Two decimals, rounded, which is what a receipt shows.

$$\texttt{1349.8920000000003}$$

The same quantity as the machine holds it. The tail is not a mistake in the program; it is what multiplying by 1.08 costs in binary.

Answer $$\boxed{\texttt{Total is 1349.89TL}\quad\text{then}\quad\texttt{Exact total is 1349.8920000000003}}$$
Check

Check the size rather than the digits: 8 per cent of 1250 is 100, so the total has to be near 1350, and it is. If a conversion had been forgotten, the program would have stopped rather than printing a plausible wrong number.

The lab papers ask for both lines on purpose. The rounded one is what a human wants and the exact one is what the machine has, and this chapter is where those two stop being the same thing.

Checkpoint
§02.1 — what comes back from input

Someone wants to print next year's age. The program below is exactly what they wrote, and the person running it types 20.

age = input('Age: ')
print(age + 1)
Find(a) Say whether the program prints 21, prints something else, or stops, and name what stops it if it stops.
Given
  • The user types the two characters 2 and 0, then Enter.

  • Nothing else in the program converts anything.

IPython console
Hint 1/4

You are not asked to fix the program, only to say what happens when it runs. So settle the type of each side of the + first and ask whether that operator has a meaning for that pair.

Hint 2/4

+ joins two strings or adds two numbers. Between a string and a number it has no meaning, and Python raises a rather than choosing one.

Hint 3/4

Here the left side is age, which came from input and is therefore text holding the two characters 2 and 0, and the right side is the number 1.

Hint 4/4

The program prints the prompt, reads the 20, and then stops with a TypeError; it never prints 21.

Show solution

Name the type on each side of the plus

$$\texttt{age}\;\text{is}\;\texttt{str}$$

It came straight out of input, and input hands back text whatever the characters are.

$$\texttt{1}\;\text{is}\;\texttt{int}$$

Written into the program as a bare number.

Decide what plus means for that pair

$$\texttt{str + int}$$

+ joins two strings or adds two numbers; it has no meaning for one of each, so the program stops with a TypeError rather than guessing.

$$\texttt{int(input(...)) + 1}$$

Converting first makes both sides numbers and the line prints 21.

Answer $$\boxed{\texttt{TypeError},\;\text{not}\;\texttt{21}}$$
Check

Compare with the counterexample above: there the types were str and int too, but the operator was , which does have a meaning for that pair. So the same mistake is silent with and loud with +.

⚠ Converting on the line after, and comparing the old name

Reading and converting feel like two jobs, so they get two lines; then there are two names for the same value and the one used below is the text.

wrong$$\texttt{n = input('n: ')}\;;\;\texttt{m = int(n)}\;;\;\texttt{if n > 10:}$$
right$$\texttt{n = int(input('n: '))}\;;\;\texttt{if n > 10:}$$
⚠ Using int on text that spells a decimal

int sounds like make this a number, so it gets used for every reading, and it works until somebody types a price or a weight.

wrong$$\texttt{int(input('kg: '))}\;\text{on}\;\texttt{72.5}\;\Rightarrow\;\texttt{ValueError}$$
right$$\texttt{int(float(input('kg: ')))}\;\Rightarrow\;\texttt{72}$$

2.2Taking one character, and taking a piece

Two index rows and one half open rule settle every index and slice expression in this course.

What came back from input is text, and nearly every question about text turns out to be a question about one of its positions.

RuleRule 2.2: one character with a number, a piece with a colon
Conditions
  • Positions run from 0 up to len(s) minus 1 counting from the left, and from -1 down to -len(s) counting from the right. Every character therefore has two names.

  • An index outside those bounds is an IndexError that stops the program. A slice outside them is trimmed silently and hands back whatever part does exist, possibly nothing at all.

  • Missing pieces have defaults: a missing start is the near end, a missing stop is the far end, and a missing step is 1.

  • With a negative step the walk runs right to left, so the start must be the right-hand one of the two or the result is empty.

$$\boxed{\texttt{s[i]}\;\text{is one character;}\quad \texttt{s[a:b]}\;\text{is}\;\texttt{s[a]},\ \dots,\ \texttt{s[b-1]}}$$

A number in the brackets picks out a single character. A colon in the brackets hands back a new piece of text that starts at the first number and stops just before the second, so its length is the second number minus the first.

Looks like this, but is not

s[4:9] on a four-character string looks like it should be an error, and s[2:2] looks like it should be the character at 2.

Neither is true. A slice is trimmed to whatever exists, so the first hands back the empty string, and a slice whose start equals its stop has length zero, so the second does too. It is the bare index s[4] that stops the program. The square brackets are the same characters in both cases and the two behaviours are opposite.

s = 'code'
print('[' + s[4:9] + ']')
print('[' + s[2:2] + ']')
print(s[4])

Sample Run:

[]
[]
IndexError: string index out of range
expressionwhat it giveswhy that

len(s)

9

A count, not a position; the last position is one less.

s[0]

i

Counting from the left starts at 0.

s[4]

a

Fifth character, because the first one is number 0.

s[-1]

n

Counting from the right starts at -1.

s[-9]

i

Same character as s[0]: -len(s) is the left end.

s[0:4]

iter

Stops before 4, so four characters, positions 0 to 3.

s[:4]

iter

A missing start means the left end, so identical to the line above.

s[4:]

ation

A missing stop means the right end, so the other five characters.

s[2:8:2]

eai

Positions 2, 4 and 6; the next would be 8, which is not below the stop.

s[3:3]

empty

Length is 3 minus 3.

s[4:100]

ation

Trimmed to what exists rather than failing.

s[::-1]

noitareti

Step -1 with both ends left out walks the whole thing backwards.

s[4:1:-1]

are

Backwards from 4, stopping before 1, so positions 4, 3 and 2.

s[9]

IndexError

One past the last position, and the bare index does not forgive.

The two halves of the table behave differently on purpose. A bare index is strict, because asking for a character that is not there has no sensible answer. A slice is forgiving, because asking for a piece that runs off the end has an obvious one: the part that exists. Checking a slice is quicker than reading it: its length is stop minus start, so s[0:4] is four characters before you look at any letters.

The middle letter, and the middle three, of a typed word

Read a word with an odd number of letters and print its middle letter, then the three letters centred on it.

word = input('Enter a word with an odd number of letters: ')
middle = len(word) // 2
print('The middle letter is ' + word[middle])
print('The middle three letters are ' + word[middle - 1:middle + 2])

Sample Run:

Enter a word with an odd number of letters: Bilkent
The middle letter is k
The middle three letters are lke
FindThe index of the middle letter in terms of the length, and the slice that gives the three letters around it.
Given
  • The word is typed by the user and has an odd number of letters.

  • For Bilkent, which has seven letters, the middle one is the fourth.

Solution

Turn the length into a position

$$\texttt{len(word) // 2}$$

For seven characters this is 3, and position 3 is the fourth character, which is the middle one. Floor division is used rather than / because a position has to be a whole number.

$$\texttt{7 // 2 = 3}$$

Worth checking on a small case: positions 0,1,2 lie to the left of 3, and 4,5,6 to the right, so 3 really is the middle.

Turn one position into a piece of three

$$\texttt{word[middle - 1:middle + 2]}$$

Start one to the left of the middle and stop two to the right, because the stop is excluded; stopping at middle + 1 would give only two letters.

$$\texttt{word[2:5]}$$

With middle = 3 this is positions 2, 3 and 4, so lke.

Answer $$\boxed{\texttt{k}\quad\text{and}\quad\texttt{lke}}$$
Check

Check the length of the slice instead of the letters: (middle + 2) - (middle - 1) is 3 whatever the word, so the expression can only ever give three characters.

The pattern is worth keeping: when a slice has to be n characters long, write the stop as start plus n and let the exclusion take care of itself.

Deciding whether a word reads the same backwards

Read a word, print it backwards, and say whether the two readings agree when capital letters are ignored.

word = input('Enter a word: ')
backwards = word[::-1]
print('Backwards: ' + backwards)
if word.lower() == backwards.lower():
    print(word + ' reads the same both ways')
else:
    print(word + ' does not read the same both ways')

Sample Run:

Enter a word: Kayak
Backwards: kayaK
Kayak reads the same both ways
FindWhy the comparison needs lower() on both sides, and what the program prints for Kayak.
Given
  • The word is typed by the user and may mix capitals and small letters, as Kayak does.

  • Reversing is available as a slice with a negative step.

Solution

Reverse without a loop

$$\texttt{word[::-1]}$$

Both ends left out and a step of -1: start at the right end and walk left. This is shorter than a loop and is the spelling the lecture slides use.

$$\texttt{Kayak}\;\rightarrow\;\texttt{kayaK}$$

The capital travels with its letter, which is exactly why the next step is needed.

Compare on a footing where case cannot interfere

$$\texttt{word.lower() == backwards.lower()}$$

Comparing the two as typed would give False here, because position 0 holds K on one side and k on the other. Lowering both sides asks the question that was meant.

$$\texttt{kayak == kayak}$$

True, so the first branch runs and the word is printed as the user typed it, not as it was compared.

Answer $$\boxed{\texttt{Backwards: kayaK}\quad\text{then}\quad\texttt{Kayak reads the same both ways}}$$
Check

Try it against a word that fails: for Ankara the reverse is arakna, the lowered forms differ at position 1, and the else branch runs. A test that says True for everything is no test.

Two habits come out of this one. Reverse with a slice, not a loop. And when a comparison of text should ignore case, lower both sides rather than one.

Checkpoint
§02.2 — four expressions on one word

The word programming has eleven characters. The program prints four values on one line, separated by the spaces that print puts in.

s = 'programming'
print(s[3], s[-3], s[3:6], s[-3:])
Find(a) Write the single line that the program prints.
Given
  • The string is programming, with positions 0 to 10.

  • Two of the four expressions are bare indices and two are slices.

IPython console
Hint 1/4

Nothing has to be computed here; the work is bookkeeping. Write the eleven characters out with a position under each one before evaluating anything.

Hint 2/4

A number in brackets gives one character. A colon gives a piece that starts at the first number and stops before the second, and a negative index counts from the right with -1 as the last character.

Hint 3/4

With s = 'programming', position 3 is the fourth character and -3 is the same as position 8. The two slices are positions 3 to 5 and positions 8 to 10.

Hint 4/4

The printed line is g i gra ing.

Show solution

Write the positions out once and reuse them

$$\texttt{p r o g r a m m i n g}$$

Positions 0 to 10 in order. Writing the string out with its indices once is faster than counting four times.

$$\texttt{s[3] = g},\;\texttt{s[-3] = s[8] = i}$$

The negative index is turned into a positive one by adding the length: -3 plus 11 is 8.

Do the two slices by length, not by letter

$$\texttt{s[3:6]}$$

Length 6 minus 3, so three characters starting at 3: gra.

$$\texttt{s[-3:]}$$

Start three from the right end with no stop, so the last three characters: ing.

Answer $$\boxed{\texttt{g i gra ing}}$$
Check

Cross-check the two slices against each other: together s[3:6] and s[-3:] must sit inside the word without overlapping, since 6 is not past 8, and they do.

⚠ Using len(s) as a position

len feels like the last one, and in a count it is, but positions start at 0 so the last one is one less.

wrong$$\texttt{s[len(s)]}\;\Rightarrow\;\texttt{IndexError}$$
right$$\texttt{s[len(s) - 1]}\;\text{or}\;\texttt{s[-1]}$$
⚠ Expecting a slice to include its stop

Ranges in ordinary speech include both ends, so s[0:4] gets read as positions 0 to 4, which is five characters.

wrong$$\texttt{s[0:4]}\;\text{read as}\;\texttt{iter}\texttt{a}$$
right$$\texttt{s[0:4] = iter},\;\text{length}\;4-0$$

2.3What a string operation hands back, and what it never changes

No operation edits a string: each one builds a new value, so a result nobody keeps is simply lost.

Indices took characters out of a string. The operations here work on the whole of it, and they all share one property that decides how they must be written.

RuleRule 2.3: strings are read-only, so every operation returns
Conditions
  • None of these operations changes the string it is called on. There is no operation in this course that does, because a string cannot be edited in place at all.

  • Therefore a call on a line of its own has no effect. The result has to be assigned to a name, printed, or used in a test.

  • The same is true of + and * on text: they build a third string and leave both operands alone.

  • The methods that ask a question, such as isalpha and isdigit, hand back True or False. The methods that search, such as find, hand back a position or -1.

$$\boxed{\texttt{t = s.lower()}\;\Longrightarrow\;\texttt{t}\;\text{is new},\;\texttt{s}\;\text{is unchanged}}$$

Calling a method on a string is asking for a second string to be built. The first one is still exactly as it was, so if you do not catch what comes back, nothing in your program has moved.

Looks like this, but is not

This looks like a program that tidies the capitals out of a name.

name = 'bILKENT'
name.lower()
print(name)
lowered = name.lower()
print(lowered)
print(name)

The second line builds a lowered copy and then throws it away, because nothing caught it. The name still points at the original characters, which the first print shows. The fourth line is the same call with a name in front of it, and only then does anything survive.

bILKENT
bilkent
bILKENT
operationhands backchanges son the empty string

len(s)

a whole number, the count of characters

no

0

s.lower()

a new string, capitals turned down

no

the empty string

s.upper()

a new string, letters turned up

no

the empty string

s.strip()

a new string with the spaces at the two ends removed, inner spaces kept

no

the empty string

s.find(t)

the position where t starts, or -1 if it is not there

no

-1 for any non empty t

s.isalpha()

True only if every character is a letter

no

False

s.isdigit()

True only if every character is a digit

no

False

s + t

a new string, the two joined with nothing between

no

the other one, unchanged

n * s

a new string, s repeated n times

no

the empty string

t in s

True or False, asking whether t appears as a run

no

True when t is empty, False otherwise

ord(c)

the code number of a single character

no

TypeError: it needs exactly one character

chr(n)

the single character with that code

no

not applicable, its argument is a number

Three lines here are worth learning as facts rather than as guesses. find returns -1 rather than stopping, so a program that does not check for -1 will happily use it as a position. isalpha on the empty string is False, not True, which matters the moment you test a piece of text that might be empty. And strip only touches the two ends: the run below shows the inner space surviving.

s = '  Ankara, 06  '
print('[' + s.strip() + ']')
print('[' + s.lower() + ']')
print('[' + s.upper() + ']')
print(s.find('Ankara'))
print(s.find('Izmir'))
print(len(s), len(s.strip()))
print('06'.isalpha(), 'Ankara'.isalpha(), 'Ankara06'.isalpha())
print(''.isalpha())
print('06'.isdigit(), ' '.isdigit())
print(ord('A'), chr(66))

Sample Run:

[Ankara, 06]
[  ankara, 06  ]
[  ANKARA, 06  ]
2
-1
14 10
False True False
False
True False
65 B

Does this word contain a vowel, without looping

Read a word and say whether it contains a vowel, using only .

word = input('Enter a word: ')
small = word.lower()
if 'a' in small or 'e' in small or 'i' in small or 'o' in small or 'u' in small:
    print(word + ' contains a vowel')
else:
    print(word + ' contains no vowel')

Sample Run:

Enter a word: Rhythm
Rhythm contains no vowel
FindWhy the test is lowered once rather than five times, and what the program answers for Rhythm.
Given
  • The word is typed and may be capitalised, as Rhythm is.

  • Only in, or and lower are needed; there is no loop.

Solution

Get the case out of the way once

$$\texttt{small = word.lower()}$$

Lowering once and keeping the result costs one name and removes the case question from all five tests below. Writing word.lower() five times would work and would be five chances to forget one.

$$\texttt{Rhythm}\;\rightarrow\;\texttt{rhythm}$$

The original is untouched, which is why the printed message can still show the word as it was typed.

Chain the five questions with or

$$\texttt{'a' in small or 'e' in small or ...}$$

or is True as soon as one side is, so this asks is any vowel present. Chaining with and would ask whether all five are, which is a different and much rarer thing.

$$\texttt{rhythm}$$

None of the five letters is in it, so every test is False and the whole or is False.

Answer $$\boxed{\texttt{Rhythm contains no vowel}}$$
Check

Test the other branch: Ankara lowers to ankara, the first test already finds an a, and the message changes. A condition that is False for everything would also print this line, so the second run is the one that proves it works.

in on a single character is the cheapest membership test in the language and it is worth reaching for before a loop. The same question with a loop appears further down, and it is longer.

Which of three typed names comes first alphabetically

Read three names and print the one that comes first when capital letters are ignored.

first = input('Enter the first name: ')
second = input('Enter the second name: ')
third = input('Enter the third name: ')
smallest = first
if second.lower() < smallest.lower():
    smallest = second
if third.lower() < smallest.lower():
    smallest = third
print(smallest + ' comes first in alphabetical order')

Sample Run:

Enter the first name: Oya
Enter the second name: Can
Enter the third name: ali
ali comes first in alphabetical order
FindThe pattern that finds a smallest value without a loop, and what happens on a tie.
Given
  • Three names are typed; they may differ only in capitalisation, as Ali and ali do.

  • Text comparison is character by character, using the character codes.

Solution

Assume the first one wins, then challenge it

$$\texttt{smallest = first}$$

Starting from the first value rather than from some invented smallest text: there is no smallest possible string to start from, so the first candidate has to be a real one.

$$\texttt{if second.lower() < smallest.lower():}$$

Each later name only has to beat the current holder, not all the others, which is why two tests are enough for three names.

See what a tie does

$$\texttt{'ali' < 'ali'}\;\text{is}\;\texttt{False}$$

A strict < means an equal name does not displace the holder, so the earlier of two equal names is the one reported. With <= the later one would win instead.

$$\texttt{print(smallest + ...)}$$

The name is printed as it was typed, because lower was only used inside the comparisons and changed nothing.

Answer $$\boxed{\texttt{ali comes first in alphabetical order}}$$
Check

Check against the character codes rather than intuition: ord('A') is 65 and ord('a') is 97, so without the lower calls every capitalised name would beat every lowercase one and Oya would win this run. The lowering is what makes the answer alphabetical rather than code order.

The three-line pattern, assume then challenge, is the same one the loops later use to find a largest value; there it runs inside a loop instead of being written out twice.

Checkpoint
§02.3 — a call whose result is not kept

A program is meant to print the name, its lowered form, its length and the position of the letter k. One of its four lines does nothing at all.

s = 'Bilkent'
s.upper()
t = s.lower()
print(s, t, len(s), s.find('k'))
Find
  1. (a) Write the printed line.

  2. (b) Name the line that has no effect and say why.

Given
  • The string is Bilkent, with seven characters.

  • One line calls a method without assigning the result anywhere.

IPython console
Hint 1/4

Read the program once looking only for lines that change something. A line that neither assigns nor prints is a good place to start.

Hint 2/4

Every string operation hands a new value back and leaves its input alone, so a call that is not assigned, printed or tested cannot affect what follows.

Hint 3/4

Here the string is Bilkent, the discarded call is the one on the second line, and the four printed things are s, t, the length, and the position of k counting from 0.

Hint 4/4

The line printed is Bilkent bilkent 7 3, and the second line of the program is the one that does nothing.

Show solution

Deal with the line that does nothing

$$\texttt{s.upper()}$$

A method call on a line of its own. It builds a new string and no name points at it, so the new string is discarded the moment the line ends.

$$\texttt{s}\;\text{is still}\;\texttt{Bilkent}$$

Because nothing can edit a string in place, the original is guaranteed unchanged.

Evaluate the four printed values

$$\texttt{t = s.lower()}\Rightarrow \texttt{bilkent}$$

This one is kept, so the new string survives under the name t.

$$\texttt{len(s) = 7},\;\texttt{s.find('k') = 3}$$

find reports a position, and positions start at 0: B is 0, i is 1, l is 2, k is 3.

Answer $$\boxed{\texttt{Bilkent bilkent 7 3}}$$
Check

Check the two numbers against each other: the position of k must be smaller than the length, and 3 is well inside 7. A find that returned 7 would be impossible and a find that returned -1 would mean the letter was absent.

⚠ Calling a method and not keeping the answer

In some languages such a call edits the value in place, and the English reading, strip the string, suggests the same thing.

wrong$$\texttt{s.strip()}\;\text{on its own line}$$
right$$\texttt{s = s.strip()}\;\text{or}\;\texttt{clean = s.strip()}$$
⚠ Reading `in` as *made of these letters*

With a single character in really does test membership, so the habit carries over to longer pieces, where it silently starts asking about a run of neighbours instead.

wrong$$\texttt{'ak' in 'ankara'}\;\text{expected}\;\texttt{True}$$
right$$\texttt{'ak' in 'ankara'}\;\text{is}\;\texttt{False};\;\texttt{'nk' in 'ankara'}\;\text{is}\;\texttt{True}$$
⚠ Using what find returns without checking for -1

find usually succeeds while you are testing, so the failure value never appears until the marker types something that is not there.

wrong$$\texttt{s[s.find('z')]}\;\text{when z is absent}$$
right$$\texttt{p = s.find('z')};\;\texttt{if p != -1:}$$

2.4Choosing a path, and the indentation that decides which

One chain runs exactly one block; two separate ifs can both run, and that difference is usually the bug.

So far every line ran, in order, once. A test lets the program skip lines, and the only thing marking which lines can be skipped is how far they are indented.

RuleRule 2.4: the first true test in a chain wins, and the rest are skipped
Conditions
  • The condition has to be something with a True or False answer. A comparison is the usual thing, but any value will do: 0, 0.0 and the empty string count as False and everything else counts as True.

  • The statements belonging to a test are the ones indented under it. The first line that returns to the outer indentation is outside the test and runs either way.

  • elif and else only attach to the if immediately above them at the same indentation. A chain is one statement with several exits, so at most one of its blocks runs.

  • Two if statements written one after the other are two separate chains. Both conditions get tested, and an else at the end belongs only to the second one.

$$\boxed{\texttt{if c1: ... elif c2: ... else: ...}\;\Longrightarrow\;\text{one block, the first with}\;\texttt{ci}\;\text{true}}$$

Read the tests from the top and stop at the first one that is true; run its block and jump past everything else in the chain. If none is true, the else block runs, and if there is no else, nothing in the chain runs at all.

Looks like this, but is not

These two programs look like the same advice written two ways.

temperature = 34
if temperature > 30:
    print('Wear a hat')
if temperature > 20:
    print('Leave the coat at home')
print('Done')

The first one prints two pieces of advice, because 34 is both above 30 and above 20 and the two tests are independent. The second prints one, because the second test is only reached when the first fails. One word changed and the number of printed lines changed with it.

Wear a hat
Leave the coat at home
Done
temperature = 34
if temperature > 30:
    print('Wear a hat')
elif temperature > 20:
    print('Leave the coat at home')
print('Done')
Wear a hat
Done
conditionvaluehow it grouped

not absent and mark >= 40

False

not binds tightest, so this is (not absent) and (mark >= 40): True and False.

not (absent and mark >= 40)

True

The brackets make not apply to the whole thing: not (False and False).

absent or mark >= 40 and mark < 50

True

and binds tighter than or, so this is absent or ((mark >= 40) and (mark < 50)): True or False.

(absent or mark >= 40) and mark < 50

False

Now the or is settled first: True and (55 < 50).

25 <= bmi < 30 with bmi 27.4

True

Python allows the chain and reads it as both comparisons joined by and, which is not true of most languages.

bool(0), bool(0.0), bool('')

all False

Zero of either kind and text with no characters count as False.

bool(' '), bool(-3)

both True

A space is a character, so that string is not empty; and any non zero number is True, negative ones included.

The first four rows are the reason this page brackets conditions even where the precedence would have done the right thing: the reader of your lab code, including you next week, should not have to recall that and binds tighter than or. The last two rows are the trap that if name: sets, because a name the user left empty is False while a name holding a single space is True.

absent = False
mark = 30
print(not absent and mark >= 40)
print(not (absent and mark >= 40))
absent = True
mark = 55
print(absent or mark >= 40 and mark < 50)
print((absent or mark >= 40) and mark < 50)

Sample Run:

False
True
True
False

Turning a lab mark into one of the five recorded bands

This course records a lab session as 0, 20, 50, 80 or 100 rather than as a raw mark. Read a mark out of 100 and print the band it is recorded as.

mark = int(input('Lab mark out of 100: '))
if mark >= 90:
    band = 100
elif mark >= 70:
    band = 80
elif mark >= 40:
    band = 50
elif mark > 0:
    band = 20
else:
    band = 0
print('That lab is recorded as', band)

Sample Run:

Lab mark out of 100: 73
That lab is recorded as 80
FindThe order the tests must be written in, and what the program prints for 73.
Given
  • The mark is a whole number typed by the user, between 0 and 100.

  • The bands are 100 from 90 up, 80 from 70 up, 50 from 40 up, 20 for anything above 0, and 0 for a mark of 0.

Solution

Pick the direction of the chain

$$\texttt{if mark >= 90:}$$

Going downwards from the top band means each later test can assume everything above it has already failed, so no test needs an upper limit. Written upwards, every test would need two comparisons joined by and.

$$\texttt{elif mark >= 70:}$$

No and mark < 90 is needed: if 90 had been reached the chain would already have finished.

Follow the run for 73

$$\texttt{73 >= 90}\;\text{is}\;\texttt{False}$$

So the first block is skipped and the chain moves on.

$$\texttt{73 >= 70}\;\text{is}\;\texttt{True}$$

The second block runs, band becomes 80, and the whole rest of the chain is jumped over even though 73 is also above 40 and above 0.

$$\texttt{print('That lab is recorded as', band)}$$

Outside the chain, at the outer indentation, so it runs whichever band was chosen.

See the same chain written upside down

$$\texttt{if mark > 0: band = 20}$$

With the tests in the other order the widest one is first and it catches everything, so the three bands below it can never be reached.

$$\texttt{73}\;\rightarrow\;\texttt{20}$$

The run proves it: the same 73 is now recorded as 20, and the program is just as quiet about it.

Answer $$\boxed{\texttt{That lab is recorded as 80}}$$
Check

Test the two boundaries, not the middle: 90 must give 100 and 89 must give 80. A chain with the comparisons written as > instead of >= passes the 89 test and fails the 90 one, which is why boundary values are the ones worth typing.

Four tests and five outcomes. One test per boundary is the least a chain like this can use.

Order is not a style question in a chain like this. The rule that comes out of it: write the tests so that no later test could also be true of a value the earlier one accepts, which for a downward chain means starting at the top.

mark = 73
if mark > 0:
    band = 20
elif mark >= 40:
    band = 50
elif mark >= 70:
    band = 80
elif mark >= 90:
    band = 100
else:
    band = 0
print('That lab is recorded as', band)
That lab is recorded as 20

Is this year a leap year

Read a year and say whether it is a leap year. A year is a leap year when it is divisible by 4, except that years divisible by 100 are not, except that years divisible by 400 are.

year = int(input('Enter a year: '))
if year % 4 == 0 and (year % 100 != 0 or year % 400 == 0):
    print(year, 'is a leap year')
else:
    print(year, 'is not a leap year')

Sample Run:

Enter a year: 1900
1900 is not a leap year
FindOne condition that covers all three rules, and the answers for 1900 and 2000.
Given
  • The year is a whole number typed by the user.

  • 1900 is divisible by 4 and by 100 but not by 400. 2000 is divisible by all three.

Solution

Turn each rule into a remainder test

$$\texttt{year \% 4 == 0}$$

Divisible by 4 is the remainder on division by 4 is zero; this is the only way this course tests divisibility.

$$\texttt{year \% 100 != 0}$$

The first exception, written as not a century.

$$\texttt{year \% 400 == 0}$$

The exception to the exception.

Join them so the exceptions sit in the right place

$$\texttt{year \% 4 == 0 and (...)}$$

Divisibility by 4 is required in every case, so it is on the outside of the and.

$$\texttt{(year \% 100 != 0 or year \% 400 == 0)}$$

Inside, the two exceptions are alternatives: either it is not a century at all, or it is one of the centuries that still counts. The brackets are doing real work here, because without them the and would bind to the first half of the or only.

Check it on the two years that matter

$$\texttt{1900}$$

Divisible by 4, so the outer test passes; 1900 % 100 is 0 so the first alternative fails; 1900 % 400 is 300 so the second fails too. The bracket is False and the answer is not a leap year.

$$\texttt{2000}$$

Divisible by 4; the first alternative fails again, but 2000 % 400 is 0, so the second one rescues it and the answer is a leap year.

Answer $$\boxed{\texttt{1900 is not a leap year}\quad\text{and}\quad\texttt{2000 is a leap year}}$$
Check

The two runs disagree, which is the point: any condition that returned the same verdict for 1900 and 2000 would be wrong whatever it said, because those two years are the whole reason the rule has exceptions.

2000 is a leap year

When a rule has exceptions, write the always-required part outside the and and the alternatives inside brackets. Trying to write the same rule as a chain of elifs also works but needs three branches instead of one condition.

Checkpoint
§02.4 — an else that belongs to the second if

Two tests are written one after the other, and an else is attached to the second one. Nothing is typed in while the program runs.

n = 15
if n % 3 == 0:
    print('three')
if n % 5 == 0:
    print('five')
else:
    print('not five')
print('end')
Find(a) Write the lines the program prints, in order.
Given
  • The value is 15, which is divisible by both 3 and 5.

  • The two if statements are separate: neither is an elif.

IPython console
Hint 1/4

Do not read the three tests as one chain. Look first at how many separate if statements there are and which one the else sits under.

Hint 2/4

Two consecutive if statements are independent: both conditions are tested. An else attaches only to the if immediately above it at the same indentation.

Hint 3/4

Here the value is 15, and 15 % 3 and 15 % 5 are both zero, so both conditions are true. The final print is at the outer indentation.

Hint 4/4

Three lines appear: three, then five, then end.

Show solution

Test the two conditions separately

$$\texttt{15 \% 3 == 0}$$

True, so the first block prints three. There is no elif here, so this does not stop anything that follows.

$$\texttt{15 \% 5 == 0}$$

Also True, so the second block prints five.

Decide who owns the else

$$\texttt{else:}$$

It sits at the same indentation as the second if and directly after its block, so it belongs to that if. Since that condition was true, the else is skipped.

$$\texttt{print('end')}$$

At the outer indentation, outside both chains, so it always runs and always last.

Answer $$\boxed{\texttt{three},\;\texttt{five},\;\texttt{end}}$$
Check

Try the value 9 in your head: the first test passes and the second fails, so the output becomes three, not five, end. A value that changes which lines appear is the check that the else really is attached to the second if.

⚠ Writing one equals sign in a condition

In ordinary mathematics one sign does both jobs, and in Python the assignment is the one people write hundreds of times a day.

wrong$$\texttt{if mark = 40:}\;\Rightarrow\;\texttt{SyntaxError}$$
right$$\texttt{if mark == 40:}$$
⚠ Writing elif where two independent tests were meant, or the reverse

Both read the same in English. If it is hot, wear a hat. If it is warm, leave the coat does not say whether the two can both apply.

wrong$$\texttt{if a: ... if b: ...}\;\text{when the cases are alternatives}$$
right$$\texttt{if a: ... elif b: ...}\;\text{for alternatives};\;\text{two ifs for independent tests}$$
⚠ Leaving the last line inside the block by accident

The editor keeps the indentation of the previous line, so the line after a block starts indented unless you move it back.

wrong$$\texttt{if mark >= 40:}\;/\;\texttt{ print('pass')}\;/\;\texttt{ print('recorded')}$$
right$$\texttt{if mark >= 40:}\;/\;\texttt{ print('pass')}\;/\;\texttt{print('recorded')}$$

2.5Repeating while a question is still true

A loop that ends has three parts: a value set up before, a test, and a change to that value inside.

A test lets the program skip a block. The same test, asked again after the block, lets it repeat one, and that is the only difference between the two statements.

RuleRule 2.5: the three parts of a loop that ends
Conditions
  • Part 1 sets up, before the loop, every name the condition reads. A name that does not exist yet cannot be tested.

  • Part 2 is the condition. It is checked before each pass, so a condition that is false at the start means the body never runs at all, not once.

  • Part 3 changes, inside the indented block, at least one name the condition reads. Without it the same answer comes back for ever.

  • Nothing guarantees the loop ends. That is the price of a while: it is the right statement exactly when the number of passes is not known in advance.

$$\boxed{\text{set up}\;\rightarrow\;\text{test}\;\rightarrow\;\text{body}\;\rightarrow\;\text{change}\;\rightarrow\;\text{test again}}$$

Check the condition. While it is true, run the indented block and come back to the condition. The loop ends the first time the condition is false, and the statement after the block picks up from there.

Looks like this, but is not

This looks like a countdown from five. It has all three parts: a set up, a test, and a line that decreases the counter.

countdown = 5
while countdown > 0:
    print('countdown is', countdown)
countdown = countdown - 1
print('lift off')

The third part is one indentation level too far left, so it is not in the loop; it is the line that runs after the loop, and the loop never gets there. The counter stays at 5, the condition stays true, and the first four lines of the run below are the first four of infinitely many identical ones.

countdown is 5
countdown is 5
countdown is 5
countdown is 5
passbalance after itcondition still true

1

10800.00

yes, 10800 is under 30000

2

11664.00

yes

3

12597.12

yes

13

27196.24

yes, still under

14

29371.94

yes, only just

15

31721.69

no, the test fails and the loop stops

Two things fall out of the table. The loop stops one pass after the balance passes the target, not on the pass that reaches it, because the condition is only asked again at the top; so the answer to how many years is the number of passes that happened, 15. And the counter has to be increased in the same block as the growth, or the two would get out of step.

How many years until a balance triples at 8 per cent

A balance of 10000 grows by 8 per cent each year. Print how many whole years pass before it is at least three times its starting value, and the balance at that point.

balance = 10000.0
rate = 8
years = 0
while balance < 3 * 10000.0:
    balance = balance * (1 + rate / 100)
    years = years + 1
print('It takes', years, 'years')
print('Balance is ' + format(balance, '.2f') + 'TL')

Sample Run:

It takes 15 years
Balance is 31721.69TL
FindThe number of years and the final balance, and why a cannot be used here.
Given
  • The starting balance is 10000 and the yearly rate is 8 per cent.

  • The target is three times the starting balance, so 30000.

Solution

A while is the only choice: the number of years is the answer, so it is not known before the loop starts, and a for loop needs that number up front.

Set the three parts up explicitly

$$\texttt{balance = 10000.0},\;\texttt{years = 0}$$

Both names the condition and the report will read, created before the loop. The balance is written with a decimal point so that the growth does not get truncated anywhere.

$$\texttt{while balance < 3 * 10000.0:}$$

The target is written as three times the start rather than as 30000, so changing the start in one place changes the target with it.

Put the growth and the count in the same block

$$\texttt{balance = balance * (1 + rate / 100)}$$

Right side worked out with the old balance, then the name moves: one pass, one year of growth.

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

Counting in the same block keeps the two in step. Counting outside would give 0 or would never stop.

Read the ending

$$\texttt{14}\;\text{passes:}\;\texttt{29371.94}$$

Still under 30000, so the condition is true again and a fifteenth pass happens.

$$\texttt{15}\;\text{passes:}\;\texttt{31721.69}$$

Now the condition fails, the loop ends, and the printed answer is 15 years.

Answer $$\boxed{\texttt{It takes 15 years},\;\texttt{Balance is 31721.69TL}}$$
Check

Check the order of magnitude without the loop: at 8 per cent a balance needs roughly nine years to double, so tripling should take rather more than double that and rather less than twenty; 15 sits where it should. As a second check, 1.08 to the 14th is under 3 and to the 15th is over it.

Fifteen passes, one multiplication and one addition each. The loop body is two lines and the whole program is seven.

Whenever the answer to the question how many times is the thing being asked for, the loop has to be a while. A for loop is for the other case, where the count is known and the result is not.

Reading numbers until a zero, then reporting the average

Read positive numbers one at a time until the user types 0, then print how many were entered and their average. Say something sensible if the very first thing typed is 0.

total = 0
count = 0
value = int(input('Enter a positive number (0 to stop): '))
while value != 0:
    total = total + value
    count = count + 1
    value = int(input('Enter a positive number (0 to stop): '))
if count == 0:
    print('No numbers were entered')
else:
    print('You entered', count, 'numbers')
    print('Their average is', total / count)

Sample Run:

Enter a positive number (0 to stop): 12
Enter a positive number (0 to stop): 7
Enter a positive number (0 to stop): 20
Enter a positive number (0 to stop): 0
You entered 3 numbers
Their average is 13.0
FindWhere the two input calls go, and why the average is guarded.
Given
  • Numbers are typed one per prompt; 0 means stop and is not one of the values.

  • The run shown types 12, 7, 20 and then 0.

Solution

The stop value has to be read before it can be tested, so the reading happens twice: once before the loop and once at the end of the body. Putting a single read at the top of the body instead is the version that counts the zero.

Read once before the loop so the condition has something to test

$$\texttt{value = int(input(...))}$$

Part 1 of the loop: the condition reads value, so value must exist first. This is the read that makes an immediate 0 work.

$$\texttt{while value != 0:}$$

The test is on the value just read, so an immediate 0 skips the body entirely and count stays at 0.

Accumulate first, then read the next one

$$\texttt{total = total + value},\;\texttt{count = count + 1}$$

The value being added is the one that has already passed the test, so the stop value can never reach the total.

$$\texttt{value = int(input(...))}$$

The last line of the body is part 3: it changes the name the condition reads. Swapping these two halves is the classic bug, and it adds the zero and misses the first value.

Guard the division

$$\texttt{if count == 0:}$$

With no values there is nothing to average, and dividing by zero would stop the program. The guard turns a crash into a sentence.

$$\texttt{total / count}\;=\;\texttt{39 / 3}$$

12 plus 7 plus 20 is 39, over three values, so the average prints as 13.0 with the decimal point, because / always gives a float.

Answer $$\boxed{\texttt{You entered 3 numbers},\;\texttt{Their average is 13.0}}$$
Check

Run it with 0 as the very first input: the body never runs, count stays 0 and the guard prints its sentence instead of dividing.

Sample Run:

Enter a positive number (0 to stop): 0
No numbers were entered

Read one, then loop on it, then read the next: that is the shape of every read-until-stop program in this course, and the version with a single read inside the body is wrong in two ways at once.

total = 0
value = int(input('Enter a number (0 to stop): '))
while value != 0:
    value = int(input('Enter a number (0 to stop): '))
    total = total + value
print('Total is', total)

Sample Run:

Enter a number (0 to stop): 5
Enter a number (0 to stop): 8
Enter a number (0 to stop): 0
Total is 8

Typing 5 then 8 then 0 should total 13, and the broken version reports 8: it skipped the 5 and added the 0.

Checkpoint
§02.5 — counting the digits of a number without turning it into text

This loop peels digits off a whole number. Nothing is typed in while it runs.

n = 17
digits = 0
while n > 0:
    n = n // 10
    digits = digits + 1
print(digits)
Find
  1. (a) Write what the program prints.

  2. (b) Say what it would print if the starting value were 0, and whether that is right.

Given
  • The starting value is 17, which has two digits.

  • Floor division by 10 removes the rightmost digit.

IPython console
Hint 1/4

You are being asked for a printed value, so trace the loop rather than reasoning about digits in general. Two columns, n and digits, one row per pass.

Hint 2/4

The body does two things each pass: it replaces n by n // 10, which removes the rightmost digit, and it adds one to the counter. The condition is checked before each pass.

Hint 3/4

Starting from n = 17 and digits = 0: after the first pass n is 1 and digits is 1; after the second, n is 0 and digits is 2.

Hint 4/4

It prints 2; and with a starting value of 0 it prints 0.

Show solution

Trace the two passes

$$\texttt{n = 17 // 10 = 1},\;\texttt{digits = 1}$$

Floor division drops the 7 rather than rounding, which is the whole reason // is used here.

$$\texttt{n = 1 // 10 = 0},\;\texttt{digits = 2}$$

One divided by ten is zero in floor division, so the last digit is counted and the value empties out.

$$\texttt{0 > 0}\;\text{is}\;\texttt{False}$$

The condition is asked a third time, fails, and the loop ends with digits at 2.

Test the boundary

$$\texttt{n = 0}$$

The condition is false before the first pass, so the body never runs and digits stays 0.

$$\texttt{while n > 0}\;\rightarrow\;\text{no passes}$$

This is not a bug in the loop but a gap in the specification, and the fix is a guard before it, in the same shape as the average guard above.

Answer $$\boxed{\texttt{2},\;\text{and}\;\texttt{0}\;\text{for}\;\texttt{n = 0}}$$
Check

Check against a longer number by hand: 8546587 should give 7, and each pass removes exactly one digit, so the count equals the number of characters the number would have as text.

⚠ The change to the counter sits outside the block

It is one line and four spaces, and the editor does not care. The program still runs, which is why this one shows up as a frozen terminal rather than as an error.

wrong$$\texttt{while c > 0:}\;/\;\texttt{ print(c)}\;/\;\texttt{c = c - 1}$$
right$$\texttt{while c > 0:}\;/\;\texttt{ print(c)}\;/\;\texttt{ c = c - 1}$$
⚠ Reading the next value at the top of the body

One read looks tidier than two, and the loop does finish, so the program looks like it works until the total is checked by hand.

wrong$$\texttt{while v != 0:}\;/\;\texttt{ v = int(input())}\;/\;\texttt{ total += v}$$
right$$\texttt{while v != 0:}\;/\;\texttt{ total += v}\;/\;\texttt{ v = int(input())}$$
⚠ Testing a float for equality in the condition

Adding 0.1 ten times obviously reaches 1.0 on paper, and the loop that never stops looks like a hang rather than a wrong answer.

wrong$$\texttt{while x != 1.0:}\;/\;\texttt{ x = x + 0.1}$$
right$$\texttt{while x < 1.0:}\;\text{or count whole steps}$$

2.6Counting loops: for and range

Use for when the number of repeats is already known; range says which numbers, and never reaches its stop.

The loop above had to be a while because the number of passes was the answer. When that number is known before the loop starts, there is a shorter statement that cannot run for ever.

RuleRule 2.6: what a range call produces
Conditions
  • With three arguments the values are start, start plus step, start plus two steps, and so on, for as long as they stay on the near side of stop. Stop is never produced.

  • With two arguments the step is 1; with one argument the start is 0 and the step is 1, so range(n) is 0 up to n minus 1.

  • A negative step counts downwards, and then near side means greater than stop. If the step points the wrong way the range is empty and the body never runs.

  • The values are worked out when the for statement begins. Changing the names that appeared in the call afterwards does not change the values the loop will visit.

$$\boxed{\texttt{range(a,b,c)}\;\Longrightarrow\;a,\;a+c,\;a+2c,\;\dots\;\text{while still on the near side of}\;b}$$

A range call names a run of numbers by saying where to begin, where to stop before, and how big a jump to take. The for statement then gives its variable each of those numbers in turn and runs the block once for each.

Looks like this, but is not

for i in range(1, len(word)): looks like the loop that visits every position of a word.

It misses two positions at once, and the program still runs. The start of 1 skips position 0, and the stop of len(word) is never reached, which is correct for the right-hand end but only because positions stop one below the length. The loop that really visits every position is range(len(word)). The two runs below are on the four letters of loop.

word = 'loop'
for i in range(1, len(word)):
    print(i, word[i])
print('---')
for i in range(len(word)):
    print(i, word[i])

Sample Run:

1 o
2 o
3 p
---
0 l
1 o
2 o
3 p
callvalues producedhow many passes

range(4)

0 1 2 3

4

range(2, 12, 3)

2 5 8 11

4, because 14 would be past 12

range(7, 10)

7 8 9

3, the step is 1 by default

range(5, 0, -1)

5 4 3 2 1

5, and 0 is not included

range(5, -1, -1)

5 4 3 2 1 0

6, which is how you get down to 0

range(3, 3)

nothing

0, start and stop are the same

range(3, 10, -1)

nothing

0, the step points away from the stop

The fourth and fifth rows are the pair worth remembering, because a countdown that has to reach 0 needs a stop of -1, which looks wrong and is right. The last two rows are the quiet ones: an empty range is not an error, so a loop whose body never runs prints nothing and complains about nothing.

for i in range(4):
    print(i, end=' ')
print()
for i in range(2, 12, 3):
    print(i, end=' ')
print()
for i in range(7, 10):
    print(i, end=' ')
print()
for i in range(5, 0, -1):
    print(i, end=' ')
print()
for i in range(5, -1, -1):
    print(i, end=' ')
print()
for i in range(3, 3):
    print(i, end=' ')
print('(nothing above)')
for i in range(3, 10, -1):
    print(i, end=' ')
print('(nothing above either)')

Sample Run:

0 1 2 3 
2 5 8 11 
7 8 9 
5 4 3 2 1 
5 4 3 2 1 0 
(nothing above)
(nothing above either)

Adding up every third number from 7 to 100

Print the sum of 7, 10, 13 and so on, taking every third number and not going past 100, and print the last number that was added.

total = 0
for n in range(7, 101, 3):
    total = total + n
print('The sum is', total)
print('The last number added was', n)
The sum is 1712
The last number added was 100
FindThe stop value the range call needs, the sum, and the last number added.
Given
  • The first number is 7 and the gap is 3.

  • The last number allowed is 100, and it may or may not be one of the numbers in the run.

Solution

A for loop is right because the numbers are fixed before the loop starts. Doing this with a while would need a set up, a test and an increase, so three places to get the bounds wrong instead of one.

Turn *not past 100* into a stop value

$$\texttt{range(7, 101, 3)}$$

The stop is excluded, so writing 100 would refuse 100 itself. Writing 101 lets 100 in if the steps happen to land on it, and refuses 101 and above.

$$\texttt{7, 10, 13, \textbackslash{}dots}$$

The run is 7 plus multiples of 3, and 100 is 7 plus 93, which is 31 threes, so 100 really is in the run.

Accumulate in the usual shape

$$\texttt{total = 0}$$

Set up before the loop, at the outer indentation. Inside it, the total would be reset on every pass and the answer would be the last number instead of the sum.

$$\texttt{total = total + n}$$

Right side first with the old total, then the name moves.

Read the loop variable after the loop

$$\texttt{print(n)}\;\text{after the block}$$

The name survives the loop and holds the last value it was given, which is 100 here. That is convenient and it is also a trap: if the range had been empty, the name would not exist at all.

Answer $$\boxed{\texttt{The sum is 1712},\;\texttt{The last number added was 100}}$$
Check

Check with the average instead of re-adding: the numbers run from 7 to 100 in equal steps, so their average is the middle of those two, 53.5, and there are 32 of them, giving 53.5 times 32, which is 1712. Two independent routes to the same number.

32 passes. The pen-and-paper route above took one multiplication, which is worth knowing when a question asks for the sum rather than for the program.

Up to and including k becomes a stop of k plus 1. This single translation accounts for a large share of the off-by-one marks in this course.

The first eight powers of three, on one line

Print the first eight powers of three, starting from 1, separated by commas, all on one line after a label.

value = 1
for power in range(8):
    if power == 0:
        line = str(value)
    else:
        line = line + ', ' + str(value)
    value = value * 3
print('The first 8 powers of 3: ' + line)
The first 8 powers of 3: 1, 3, 9, 27, 81, 243, 729, 2187
FindHow the separator is kept out of the ends, and the printed line.
Given
  • The first power is 1, which is three to the power zero.

  • The values must appear on one line, separated by a comma and a space, with no comma before the first or after the last.

Solution

The line is built up in a string and printed once at the end, rather than printed piece by piece with end=''. Either works; building the string makes the no comma at the ends rule easy to see in one place.

Make the count the loop and the value a running product

$$\texttt{for power in range(8):}$$

Eight passes, and the variable is used only to count. The value itself is not power but the number being multiplied up beside it.

$$\texttt{value = value * 3}$$

One multiplication per pass, so the eight values are 1, 3, 9 and so on without ever using an exponent.

Treat the first item differently from the rest

$$\texttt{if power == 0: line = str(value)}$$

The first item starts the line, with no separator in front of it. This is where the no comma at the front rule lives.

$$\texttt{else: line = line + ', ' + str(value)}$$

Every later item brings its own separator in front, which is why there is no comma left dangling at the end either.

$$\texttt{str(value)}$$

The conversion is needed because + between text and a number has no meaning.

Answer $$\boxed{\texttt{The first 8 powers of 3: 1, 3, 9, 27, 81, 243, 729, 2187}}$$
Check

Two checks. The last value should be three to the seventh, since the first is three to the zeroth, and 3 to the 7 is 2187. And the line should hold seven commas for eight items, which it does.

Separators belong in front of every item except the first, not behind every item. The alternative, printing with end=', ', leaves a comma and a space hanging after the last value, and on paper that costs the mark.

Checkpoint
§02.6 — how many passes, and what they add up to

A total is built from one range call. Nothing is typed in while the program runs.

total = 0
for k in range(3, 20, 4):
    total = total + k
print(total)
Find
  1. (a) List the values the loop visits.

  2. (b) Write the number the program prints.

Given
  • The call is range(3, 20, 4).

  • The total starts at 0 and each value is added once.

Hint 1/4

Do not add anything yet. The first job is to write down which numbers the loop actually visits, because the bounds are where this question is won or lost.

Hint 2/4

A range call gives start, start plus step, and so on, for as long as the value stays on the near side of stop, and the stop itself is never one of them.

Hint 3/4

Here start is 3, step is 4 and stop is 20, so the run is 3, 7, 11, 15, 19; the next would be 23.

Hint 4/4

Five values are visited and the printed total is 55.

Show solution

Write the run out before adding anything

$$\texttt{3, 7, 11, 15, 19}$$

Start at 3 and keep adding 4 while the value stays below 20. Listing them first turns the question into arithmetic and removes the bounds from the sum.

$$\texttt{23 \textbackslash{}geq 20}$$

The first value that fails the test, so it never appears and the run has five members.

Add them the short way

$$\texttt{3 + 19 = 22},\;\texttt{7 + 15 = 22}$$

Pairing from the ends: equal steps mean every such pair has the same total, which also catches a miscount.

$$\texttt{22 + 22 + 11 = 55}$$

Two pairs and the middle value left over, giving 55.

Answer $$\boxed{\texttt{55}}$$
Check

Cross-check with the average: five values with a middle of 11 give 5 times 11, which is 55. If the count had been wrong, the two routes would disagree.

⚠ Writing the stop as the last value you want

From 1 to 10 includes 10 in every other context, so the exclusion has to be learned rather than guessed.

wrong$$\texttt{range(1, 10)}\;\text{for}\;1\dots 10$$
right$$\texttt{range(1, 11)}\;\text{for}\;1\dots 10$$
⚠ A countdown that stops one short of zero

With a negative step the stop still excludes itself, so the value you want to reach has to be written one further out, which looks like a typing mistake.

wrong$$\texttt{range(5, 0, -1)}\;\Rightarrow\;5,4,3,2,1$$
right$$\texttt{range(5, -1, -1)}\;\Rightarrow\;5,4,3,2,1,0$$
⚠ Changing the bound inside the loop and expecting the loop to notice

The call looks like it is re-read each pass, the way a while condition is. It is not: the values were settled when the for began.

wrong$$\texttt{for i in range(n): n = 1}\;\text{expected to stop early}$$
right$$\texttt{break}\;\text{is how a for stops early}$$

2.7A loop inside a loop, and the two ways out

The inner loop finishes for every single step of the outer one, and break leaves only the loop it is in.

One loop walks a line of values. Two loops, one inside the other, walk every pair, and the order they are walked in is the thing a tracing question tests.

RuleRule 2.7: the inner loop runs to the end for each single pass of the outer one
Conditions
  • For each value the outer variable takes, the inner loop starts again from its own beginning and runs to its own end. So the total number of passes is the product, not the sum.

  • The inner range is worked out again at the start of each inner loop, so if it mentions the outer variable it can be a different run each time. The outer range is worked out once.

  • break ends the innermost loop that contains it and nothing more. The statement after that loop, still inside the outer one, is where the program continues.

  • There is no statement in this course that leaves two loops at once. Leaving both takes a name that records what happened plus a second test after the inner loop.

$$\boxed{\texttt{for i in ...: for j in ...: body}\;\Longrightarrow\;\text{body runs}\;(\text{outer count})\times(\text{inner count})\;\text{times}}$$

Fix the outer variable at its first value and run the whole inner loop; then move the outer variable on and run the whole inner loop again. A break inside the inner loop cuts that one run short and hands control back to the outer loop, which carries on as normal.

Looks like this, but is not

This looks like a program that stops as soon as it finds a pair adding up to 2.

found = 'no'
for i in range(3):
    for j in range(3):
        if i + j == 2:
            found = str(i) + str(j)
            break
print(found)

It finds that pair three times and reports the last one. The break ends the inner loop, the outer loop then moves on as if nothing had happened, and each new outer pass overwrites the answer. The printed result is 20, from i = 2 and j = 0, not the 02 that the first find produced.

20
passijlimit after this pass

1

0

0

1

2

0

1

1

3

0

2

1

4

1

0

1

5

2

0

1

Five passes, not nine. The outer range was fixed at range(3) when the outer for began, so i still runs 0, 1, 2 even though limit is 1 from the first pass onwards. The inner range is built again at the start of each inner loop, so from the second outer pass on it is range(1) and gives a single value. Reassigning the bound changed the inner loop and had no effect at all on the outer one.

limit = 3
for i in range(limit):
    for j in range(limit):
        print('i =', i, 'j =', j)
        limit = 1

Sample Run:

i = 0 j = 0
i = 0 j = 1
i = 0 j = 2
i = 1 j = 0
i = 2 j = 0

A triangle whose rows are made of their own row number

Read a height and print that many rows, where row number k holds the digit k repeated k times, separated by spaces.

height = int(input('How many rows? '))
for row in range(1, height + 1):
    for column in range(row):
        print(row, end=' ')
    print()

Sample Run:

How many rows? 4
1 
2 2 
3 3 3 
4 4 4 4 
FindWhich loop owns the line break, and the exact four lines printed for a height of 4.
Given
  • The height is typed by the user; the run uses 4.

  • Row 1 has one item, row 2 has two, and so on.

Solution

The inner loop counts items within a row and the outer loop counts rows, so the number of items has to depend on the outer variable. That is the whole idea: the inner range mentions the outer name.

Make the rows one-based so the row number is printable

$$\texttt{range(1, height + 1)}$$

Row numbers are 1 to height, so the start is 1 and the stop is one past the last row. range(height) would give rows numbered 0 to 3 and the first row would print nothing.

$$\texttt{range(row)}$$

The inner loop needs row passes and its variable is never used, so the plain one-argument form is enough.

Put the line break in the outer loop

$$\texttt{print(row, end=' ')}$$

Inside the inner loop, so every item is written with a space after it and the line stays open.

$$\texttt{print()}$$

Indented under the outer loop but outside the inner one, so it runs once per row and closes that row's line. Moving it one level in would end the line after every single item.

Read the exact characters

$$\texttt{1 \textbackslash{}\_}$$

Row 1 is the digit 1 followed by the space that end put there, so the line ends in a space; that trailing space is part of the output.

$$\texttt{4 4 4 4 \textbackslash{}\_}$$

Row 4 has four items, each followed by a space, so ten characters in all.

Answer $$\boxed{\texttt{1 / 2 2 / 3 3 3 / 4 4 4 4}\;\text{, each line ending in a space}}$$
Check

Count the items rather than reading them: rows of 1, 2, 3 and 4 items make 10 printed values for a height of 4, and the triangle has the shape it should. A version with the line break in the wrong place would print 10 lines instead of 4, which is visible at a glance.

Ten passes of the inner body for a height of four, and in general the height times the height plus one, over two.

In any nested printing loop, ask which loop owns the line break before writing either of them. That one decision fixes the shape of the output.

The first letter two typed words share

Read two words and report the first letter of the first word that also occurs in the second, together with its position. Say so plainly if they share nothing.

first = input('Enter the first word: ')
second = input('Enter the second word: ')
position = -1
shared = ''
for i in range(len(first)):
    for j in range(len(second)):
        if first[i] == second[j]:
            position = i
            shared = first[i]
            break
    if position != -1:
        break
if position == -1:
    print('The two words share no letter')
else:
    print('The first shared letter is ' + shared + ' at position', position)

Sample Run:

Enter the first word: python
Enter the second word: bilkent
The first shared letter is t at position 2
FindHow both loops are left once a letter is found, and the answer for that pair.
Given
  • Two words are typed; the run uses python and bilkent.

  • The answer wanted is the first such letter in the first word, not just any shared letter.

Solution

The outer loop walks the first word because the question asks for the first letter of that word; walking the second word on the outside would answer a different question. A break alone is not enough, so a recorded position does the second half of the job.

Record the find rather than just stopping

$$\texttt{position = -1}$$

A value that cannot be a real position, set up before both loops. It is both the not found yet flag and the final answer, which is why -1 is the conventional choice: find uses it for the same purpose.

$$\texttt{position = i},\;\texttt{shared = first[i]}$$

Both recorded at the moment of the find, while i and j still hold the right values.

Leave the inner loop, then leave the outer one

$$\texttt{break}\;\text{inside the inner loop}$$

Ends the search through the second word for this letter only.

$$\texttt{if position != -1: break}$$

Indented under the outer loop, after the inner one. This is the second half of leaving both loops, and without it the outer loop would keep going and later letters would overwrite the answer, exactly as in the counterexample above.

Follow the run on python and bilkent

$$\texttt{p},\;\texttt{y},\;\texttt{t}$$

p is not in bilkent, nor is y; t is, at position 6 of the second word.

$$\texttt{position = 2}$$

So the first shared letter is t and its position in the first word is 2, counting from 0.

Answer $$\boxed{\texttt{The first shared letter is t at position 2}}$$
Check

Check the no-overlap case, which is the one the -1 guard exists for: with xyz and abc the inner loop never finds anything, the outer loop finishes normally, and position is still -1.

first = 'xyz'
second = 'abc'
position = -1
for i in range(len(first)):
    for j in range(len(second)):
        if first[i] == second[j]:
            position = i
            break
    if position != -1:
        break
print(position)

Sample Run:

-1

Up to the length of one word times the length of the other, but the two breaks stop it at the first find: 3 letters of the first word were examined here, not 6.

Leaving two loops takes three pieces: a name set to an impossible value, a break in the inner loop, and a test plus break after it. Any one of the three missing gives a program that runs and answers a different question.

Checkpoint
§02.7 — how far the inner loop gets

Two loops with a break in the inner one. Nothing is typed in while the program runs.

for i in range(2):
    for j in range(3):
        if j == 1:
            break
        print(i, j)
Find
  1. (a) Write the lines the program prints, in order.

  2. (b) Say how many times the print statement is reached out of the six pairs the two loops describe.

Given
  • The outer range is range(2) and the inner is range(3).

  • The break is reached when the inner variable is 1, before the print.

IPython console
Hint 1/4

Six pairs are possible, but the question is how many actually happen. Trace it with one column for i and one for j and stop each row where the program stops.

Hint 2/4

A break ends the innermost loop containing it, so the outer loop continues with its next value. Statements after the break in the same body are skipped for that pass.

Hint 3/4

Here i runs 0 then 1, and for each of those j runs 0 and then 1, where the break fires before the print.

Hint 4/4

Two lines are printed, 0 0 and 1 0, so the print is reached twice out of six possible pairs.

Show solution

Take the outer passes one at a time

$$\texttt{i = 0}:\;\texttt{j = 0}$$

The break test fails, so the print runs and writes 0 0.

$$\texttt{i = 0}:\;\texttt{j = 1}$$

The break test succeeds, so the inner loop ends here and the print is skipped. j never becomes 2.

Confirm the outer loop survives

$$\texttt{i = 1}$$

The break left only the inner loop, so the outer loop takes its next value as normal.

$$\texttt{i = 1}:\;\texttt{j = 0},\;\text{then break}$$

The same two passes happen again, giving the second printed line and then stopping the inner loop.

Answer $$\boxed{\texttt{0 0}\;\text{then}\;\texttt{1 0},\;2\;\text{of}\;6}$$
Check

Count the passes a different way: the inner loop gets two passes per outer value, one that prints and one that breaks, so 2 times 2 is 4 passes of the body and exactly half of them print.

⚠ Expecting break to leave both loops

In English stop means stop, and the find really has happened, so there seems to be nothing left to do.

wrong$$\texttt{for i: for j: if hit: break}\;\text{expected to end the search}$$
right$$\texttt{for i: for j: if hit: break}\;/\;\texttt{ if hit: break}$$
⚠ Reassigning the outer bound to end the outer loop early

A really does re-read its condition each pass, so the same move looks available in a for.

wrong$$\texttt{for i in range(n): n = 1}$$
right$$\texttt{for i in range(n): if done: break}$$
Tracing a program by hand, the way the paper wants it

Any question that says what is the output. It is also the fastest way to find your own bug when the program runs but prints the wrong thing.

  1. List the names

    Write one column for every name the program assigns, plus one last column headed printed. Do this before reading the loop, so you are not inventing columns half way through.

  2. Fill in the set up

    Put the values from the lines above the loop into the first row. A name with no value yet is a gap, and a gap that the loop reads is already a bug.

  3. One row per pass, condition first

    For each pass, write the condition and its answer before you write what the body does. This is what stops you running one pass too many, which is the most common tracing error.

  4. Write printed characters, not descriptions

    In the printed column put the actual characters, with their spaces. Mark whether the line was ended: a print with end='' leaves the line open and the next print continues on it.

  5. Mark the exit and copy the block out

    Write the row where the condition first fails, then copy the printed column into one block. That block, including blank lines and trailing spaces, is the answer.

Where it goes wrong
  • Tracing the body and forgetting the condition is re-asked after it, which gives one pass too many.

  • Treating a range as if it were re-read each pass when the loop reassigns the bound; the values were fixed when the for began.

  • Writing prints the numbers instead of the numbers. That is not an answer to this question type.

  • Losing a trailing space or a blank line, which on paper is the difference between full marks and most of them.

Choosing between for and while

Before writing either. Getting this wrong does not stop the program; it just makes the loop twice as long and gives the bounds two more places to hide.

  1. Ask whether the count is known before the loop

    If you can say how many passes there will be using only what the program already holds, that is a for. If the count is itself the answer, or depends on what the user types next, that is a while.

  2. For a for, write the range last

    Write the body first with the variable in it, then settle start, stop and step. Up to and including k becomes a stop of k plus 1; every position of s becomes range(len(s)).

  3. For a while, write the three parts as three lines

    Set up above the loop, condition in the header, change as the last line of the body. Writing them in that order means the loop is finished before you start worrying about what it computes.

  4. Check the zero-pass case in both

    An empty range and a condition that is false at the start both mean the body never runs. Decide what the program should print then, and write that branch.

Where it goes wrong
  • Using a for and then breaking out of it on a condition, which is a while wearing a disguise.

  • Using a while for a fixed count, then forgetting the increase and hanging the terminal.

  • Walking a string by index when the characters alone are needed; for ch in s is shorter and cannot go out of bounds.

The read-until-stop shape

Whenever the number of values is decided by the person typing them. Three of the exercises in the first two lab sessions are this shape.

  1. Set the up

    A total, a count, or whatever the report needs, all at zero and all above the loop.

  2. Read once before the loop

    The condition has to test something, so the first value is read before the loop starts. This is also what makes an immediate stop value work.

  3. Loop on the value, not on a counter

    The header is while value != stop:. Nothing else belongs in that condition.

  4. Use the value, then read the next one

    In that order, as the last two things in the body. The value being used has already passed the test, so the stop value can never be counted.

  5. Guard the report

    If nothing was entered, the count is zero and any average would divide by it. Print a sentence instead.

Where it goes wrong
  • One read at the top of the body instead of two reads: the first value is skipped and the stop value is counted.

  • Testing a counter in the header as well, which silently caps the number of values.

  • Dividing before checking the count, which turns an empty run into a crash.

The same sum with a for loop

Add up the multiples of 4 that lie between 10 and 60 inclusive.

total = 0
for n in range(12, 61, 4):
    total = total + n
print('Sum of the multiples of 4 from 10 to 60 is', total)
Sum of the multiples of 4 from 10 to 60 is 468
FindThe sum, and where the bounds are written.
Given
  • The first multiple of 4 at or above 10 is 12.

  • The last one at or below 60 is 60 itself.

Solution

All three bounds live in one call

$$\texttt{range(12, 61, 4)}$$

Start, stop and step side by side, so the whole question of which numbers is settled in one place and can be checked at a glance.

$$\texttt{61}\;\text{not}\;\texttt{60}$$

60 has to be included, and the stop is excluded, so the stop is written one past it.

The body has one job

$$\texttt{total = total + n}$$

Nothing in the body touches the loop variable, so there is no way for the loop to fail to end.

Answer $$\boxed{\texttt{468}}$$
Check

Pair from the ends: 12 plus 60 is 72, 16 plus 56 is 72, and with 13 numbers there are 6 such pairs plus the middle one, 36. Six 72s plus 36 is 468.

The same sum with a while loop

The same task, written with a while loop instead.

total = 0
n = 12
while n <= 60:
    total = total + n
    n = n + 4
print('Sum of the multiples of 4 from 10 to 60 is', total)
Sum of the multiples of 4 from 10 to 60 is 468
FindThe sum, and how many separate places now hold a bound.
Given
  • The same bounds: first multiple 12, last one 60.

  • The same accumulator starting at 0.

Solution

The bounds are spread over three lines

$$\texttt{n = 12}$$

The start, above the loop.

$$\texttt{while n <= 60:}$$

The end, in the header, and written with <= because this time 60 must be allowed in. A < here would drop 60 silently.

$$\texttt{n = n + 4}$$

The step, at the bottom of the body. Forgetting this one line does not change the answer, it hangs the program.

Answer $$\boxed{\texttt{468}}$$
Check

Same answer as the for version, which is the point: the two loops are equivalent here, so any difference in the printed number would mean one of the three bounds had been mistranslated.

Both print 468, and both are correct; the difference is that the for version states the three bounds in one call where a reader can check them together, while the while version spreads the same three across three lines, one of which can be forgotten without any error message.

How to tell them apart

Ask whether the loop could be written as a range before writing anything. If it can, the for version is shorter and cannot run for ever, and it is the one to write. Keep the while for the case where the number of passes depends on something the loop itself discovers, such as a value typed next or a balance passing a target.

One character with a bare index

What s[2] gives on the word compile, and how it behaves.

s = 'compile'
print(s[2])
print(type(s[2]))
print(len(s[2]))
m
<class 'str'>
1
FindThe value, its type and its length.
Given
  • The string is compile, seven characters, positions 0 to 6.

  • The expression is a bare index with no colon.

Solution

Read the value

$$\texttt{s[2] = m}$$

Position 2 is the third character, because counting starts at 0.

$$\texttt{type}\;\text{is}\;\texttt{str},\;\texttt{len}\;\text{is}\;1$$

There is no separate character type in Python: a single character is a string of length one.

Answer $$\boxed{\texttt{m},\;\texttt{str},\;\text{length}\;1}$$
Check

The type and the length agree with each other, which is the check: anything of length 1 that is not a string would be a different language.

One character with a slice

What s[2:3] gives on the same word, and how it behaves when the position does not exist.

s = 'compile'
print(s[2:3])
print(type(s[2:3]))
print(len(s[2:3]))
print('[' + s[9:12] + ']')
m
<class 'str'>
1
[]
FindThe value, its type, its length, and what happens off the end.
Given
  • The same string compile.

  • The expression is a slice of length 3 minus 2.

Solution

Read the value, which looks identical

$$\texttt{s[2:3] = m}$$

Length 3 minus 2 is 1, and the one character it contains is at position 2, so it prints exactly as the bare index did.

$$\texttt{type}\;\text{is}\;\texttt{str}$$

Same type, same length, same printed character: nothing on screen tells the two apart.

Go off the end and the difference appears

$$\texttt{s[9:12]}\;\text{is empty}$$

A slice is trimmed to what exists, so this hands back a string of length zero and the program continues.

$$\texttt{s[9]}\;\Rightarrow\;\texttt{IndexError}$$

The bare index has nothing to hand back and stops the program instead.

Answer $$\boxed{\texttt{m},\;\text{then}\;\texttt{[]}\;\text{for the empty slice}}$$
Check

The empty slice is printed here between square brackets on purpose: without them, a line holding nothing is impossible to tell from a line holding a space.

On a position that exists the two expressions are indistinguishable, same character, same type, same length; off the end one of them quietly hands back nothing and the other stops the program.

How to tell them apart

Use the bare index when the position must exist and you would rather find out loudly if it does not, which is the normal case inside a loop over range(len(s)). Use the slice when running off the end is a possibility you want handled quietly, for example when taking the first three characters of text that might be shorter than three.

Scaffolding comes off
The common skeleton
  1. Set up an accumulator above the loop: a counter at 0, or an empty string, or a position at -1. Above the loop, so it survives all the passes.

  2. Decide whether the loop needs the characters or their positions. for ch in s gives characters; for i in range(len(s)) gives positions and lets you use s[i].

  3. Write one test inside the loop that says which characters count.

  4. Update the accumulator in the branch where the test succeeded, and only there.

  5. Report after the loop, at the outer indentation, and handle the case where nothing ever matched.

1 · fully worked

Counting the vowels in a typed word

Read a word and print how many vowels it contains, ignoring capitals.

word = input('Enter a word: ')
vowels = 'aeiou'
count = 0
for ch in word.lower():
    if ch in vowels:
        count = count + 1
print(word + ' has', count, 'vowels')

Sample Run:

Enter a word: Cumhuriyet
Cumhuriyet has 4 vowels
FindThe count, and where each of the five skeleton steps appears in the program.
Given
  • The word is typed and may be capitalised, as Cumhuriyet is.

  • The five vowels are a, e, i, o and u.

Solution

Accumulator and the case question, both before the loop

$$\texttt{count = 0}$$

Skeleton step 1. Above the loop, so the passes add to the same counter instead of each starting a new one.

$$\texttt{vowels = 'aeiou'}$$

Naming the set once means the test below reads as a single question rather than as five.

Walk the characters, not the positions

$$\texttt{for ch in word.lower():}$$

Skeleton step 2. The positions are not needed here, only the letters, so this is the shorter of the two forms and cannot go out of bounds.

$$\texttt{word.lower()}$$

Lowering the string the loop walks, rather than lowering inside the test, does the case work once instead of once per character.

One test, one update

$$\texttt{if ch in vowels:}$$

Skeleton step 3. A membership test on a single character, which is the cheapest way to ask this.

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

Skeleton step 4, inside the branch where the test succeeded. One level out and every character would be counted.

Report outside the loop

$$\texttt{print(word + ' has', count, 'vowels')}$$

Skeleton step 5, at the outer indentation, so it runs once and not once per character. The word is printed as typed, because lower built a separate string.

$$\texttt{Cumhuriyet}$$

The vowels are u, u, i and e, so four.

Answer $$\boxed{\texttt{Cumhuriyet has 4 vowels}}$$
Check

Count the consonants as a cross-check: the word has ten letters, and C, m, h, r, y and t are the six that are not vowels, so four vowels is consistent.

Every rung below is this same program with one part changed. Keeping the five skeleton steps in the same order is what makes the next one quick.

2 · you write the reasoning

Same skeleton, easier test: read a plate number and print how many of its characters are digits. The steps are below with the reasoning removed. Write your own reason for each one before opening it, and note that you are not being asked to invent any code here, only to say why each line is where it is.

text = input('Enter a plate number: ')
count = 0
for ch in text:
    if ch.isdigit():
        count = count + 1
print(text + ' has', count, 'digits')

Sample Run:

Enter a plate number: 06ABC1234
06ABC1234 has 6 digits
  1. count = 0 is written above the loop.

    reasoning

    Above the loop because it has to survive every pass. Inside, it would be reset to 0 at the start of each pass and the final answer would be 0 or 1.

  2. The loop is written as for ch in text: rather than over a range of positions.

    reasoning

    Because only the characters matter, not where they are. The position form would work too but needs text[i] in the test and gives one more place to write the wrong index.

  3. The test is ch.isdigit() with no comparison and no == True.

    reasoning

    Because isdigit already hands back True or False, so it is already a condition. Writing == True adds a comparison that can only ever agree with what it was given.

  4. count = count + 1 is indented under the test, not under the loop.

    reasoning

    Because the counter must only move for the characters that passed the test. One indentation level out and it would count every character, so the answer would always equal the length.

  5. Nothing lowers the text this time, although the vowel version did.

    reasoning

    Because digits have no capital and small forms, so there is nothing for lower to do. In the vowel version the letters did, which is the only reason it was there.

3 · find the buried error

Harder, and now the program below is somebody else's work. It is supposed to read a word, print the string of all its characters that are not vowels, and print the position of the first vowel, or -1 if there is none. Run with Bilkent it should print Blknt and 1. It prints something else. Exactly two of the steps are wrong.

word = input('Enter a word: ')
vowels = 'aeiou'
kept = ''
position = -1
for i in range(1, len(word)):
    if word[i].lower() in vowels:
        position = i
    else:
        kept = kept + word[i]
print('Consonants: ' + kept)
print('First vowel at position', position)

Sample Run:

Enter a word: Bilkent
Consonants: lknt
First vowel at position 4
  1. Read the word and name the five vowels.

  2. Start an empty string for the kept characters and a position of -1 for not found.

  3. Walk the positions of the word with for i in range(1, len(word)):.

  4. If the character at i is a vowel, record its position with position = i.

  5. Otherwise add the character to the kept string.

  6. After the loop, print the kept string and the position.

the two buried errors (2)
⚠ step 3

The range starts at 1, so position 0 is never looked at. On Bilkent that loses the B from the consonant string, which is why the program prints lknt instead of Blknt. It would also miss a vowel sitting at position 0 entirely.

Counting from 1 is what everyone does when saying the first letter, and the loop runs without complaint, so the only symptom is one missing character at the front.

right

for i in range(len(word)):

⚠ step 4

position = i runs for every vowel, so each one overwrites the last and the value left at the end is the position of the final vowel, not the first. On Bilkent that prints 4, the e, instead of 1, the i.

The line is true of every vowel, and while testing on a word with only one vowel it gives the right answer, so the bug stays hidden until a second vowel appears.

right

Guard it so only the first find is recorded: if position == -1: position = i

4 · the bare problem
§02.7 — counting and locating one character in typed text

Nothing is scaffolded this time. Write a program, Sec02_Q4.py, that reads a line of text and then one character, and prints how many times that character occurs in the text and the position of the last occurrence, or -1 if there is none. Positions count from 0 and spaces count as characters.

Then give the exact output for the run shown below.

Find
  1. (a) Write the program.

  2. (b) Write the exact two lines it prints for that run.

  3. (c) Say what it prints if the character typed never occurs in the text.

Given
  • The text typed is ankara ankara, thirteen characters including the space at position 6.

  • The character typed is a.

  • The answer for the last position must be a position in the text, or -1 when the character never occurs.

Hint 1/4

Two separate questions are being asked about the same walk, so before writing any loop decide what each answer needs to survive the loop: one counter and one position.

Hint 2/4

Walk the positions with for i in range(len(sentence)): so that i is available as an answer, and compare sentence[i] == target inside.

Hint 3/4

Update both accumulators in the branch where the comparison succeeded. The text is ankara ankara and the target is a, which occurs at 0, 3, 5, 7, 10 and 12.

Hint 4/4

It prints a appears 6 times and then The last one is at position 12.

Show solution

Set up two accumulators of different kinds

$$\texttt{count = 0}$$

A counter, because the first question is how many.

$$\texttt{last = -1}$$

A position with an impossible value, because the second question is where, and -1 has to survive the case where the answer is nowhere.

Walk positions, not characters

$$\texttt{for i in range(len(sentence)):}$$

The answer includes a position, so the loop variable has to be the position. Walking characters would give the count but not the place.

$$\texttt{if sentence[i] == target:}$$

A plain comparison, since the target is a single character typed by the user.

Update both accumulators in the same branch

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

One per match.

$$\texttt{last = i}$$

No guard this time, and deliberately: the question asks for the last occurrence, so overwriting on every match is exactly right. The first-occurrence version is the one that needs the guard.

Read the run

$$\texttt{a}\;\text{at}\;0,3,5,7,10,12$$

Six matches across the thirteen characters, with the space at position 6 matching nothing.

$$\texttt{count = 6},\;\texttt{last = 12}$$

And 12 is the last position of a thirteen-character string, which is consistent.

Answer $$\boxed{\texttt{a appears 6 times},\;\texttt{The last one is at position 12}}$$
Check

Check the position against the length: the text has 13 characters, so any reported position must be between 0 and 12, and 12 is exactly the right-hand end. Then check the count against the two halves: ankara holds three as and the text holds it twice.

First occurrence needs a guard, last occurrence needs none. Deciding which of the two the question asks for, before writing the update, is the whole difference between this rung and the one above it.

Full exam-style question

One program, three structures, five printed linesexam format

This is the shape the tracing question on this course's paper takes: a short program with no input, mixing a while loop, an if chain and a for loop, and the marks are for the characters. Write the exact output before reading on.

s = 'CS115'
i = 0
tally = 0
while i < len(s):
    ch = s[i]
    if ch.isdigit():
        tally = tally + int(ch)
        print(ch, end='+')
    elif ch == 'S':
        print('[S]', end='')
    else:
        print(ch.lower(), end='')
    i = i + 2
print()
print('tally =', tally)
for k in range(len(s), 0, -2):
    print(k, s[k - 1])
FindThe five printed lines, exactly, and one branch of the chain that never runs.
Given
  • The string is CS115, five characters, positions 0 to 4.

  • i starts at 0 and grows by 2 each pass, so it takes the values 0, 2 and 4.

  • print(ch, end='+') leaves the line open; the bare print() after the loop closes it.

Solution

Work out which positions the while loop visits

$$\texttt{i = 0, 2, 4}$$

The step is 2, so only the even positions are looked at; the first thing to settle, because everything else depends on it.

$$\texttt{i = 6}\;\text{fails}\;\texttt{i < 5}$$

So three passes, not five.

Take the three passes through the chain

$$\texttt{ch = C}$$

Not a digit and not S, so the else branch runs and writes c with no line ending, because end=''.

$$\texttt{ch = 1}$$

A digit, so tally becomes 0 plus 1, and 1+ is written, still on the same line.

$$\texttt{ch = 5}$$

A digit, so tally becomes 1 plus 5, which is 6, and 5+ is written.

Notice the branch that cannot be reached

$$\texttt{elif ch == 'S'}$$

S is at position 1, and the loop only ever visits 0, 2 and 4, so this branch never runs. A branch that looks live and is dead is a favourite thing to put in this question type.

$$\texttt{print()}$$

After the loop, so the open line is closed and the first printed line is complete: c1+5+.

Then the for loop, counting down in twos

$$\texttt{range(5, 0, -2)}\;\Rightarrow\;5, 3, 1$$

Start at 5, step -2, stop before 0, so three values. 0 itself is excluded, which is why the last value is 1 and not -1.

$$\texttt{k = 5: s[4] = 5}$$

The index is k - 1, so this prints 5 5: the counter and the character, with print's single space between them.

$$\texttt{k = 3: s[2] = 1}$$

Prints 3 1.

$$\texttt{k = 1: s[0] = C}$$

Prints 1 C.

Answer $$\boxed{\begin{aligned}&\texttt{c1+5+}\\&\texttt{tally = 6}\\&\texttt{5 5}\\&\texttt{3 1}\\&\texttt{1 C}\end{aligned}}$$
Check

Two independent checks. The tally must equal the sum of the digits the while loop actually saw, which is 1 plus 5, and the printed 6 agrees. And the number of lines must be one for the closed end='' line, one for the tally, and one per value of the second range, which is 1 plus 1 plus 3, so five lines.

c1+5+
tally = 6
5 5
3 1
1 C

Three passes of the while loop and three of the for loop, six in all, which is the size these questions are set at.

Three habits pay for themselves on this question type. Settle the loop variable's values before touching the body. Track whether the line is open or closed after every print. And check each branch of a chain against the values the loop can actually produce, because a dead branch is a trap, not a mistake in the question.

Practice

A · concept 4 questions
1§02.1 — the type that comes back from input

A first program reads a quantity and then does arithmetic with it. Decide whether the claim below is true, and give the reason in one sentence.

Find(a) True or false, with the reason.
Given
  • The claim: if the user types nothing but digits, input() hands back a number, so no conversion is needed.

  • The user types 7 and presses Enter.

Hint 1/4

You are being asked about a type, not about a value, so the question is what decides the type of what input hands back.

Hint 2/4

The type is fixed by the function, not by the characters: input always hands back a str, and Python never guesses a conversion.

Hint 3/4

Here the user types the single character 7, so the value is the one-character text 7 and not the number 7.

Hint 4/4

The claim is false, and the shortest proof is that multiplying by 3 gives 777.

Show solution

Separate the characters from the type

$$\texttt{type(answer)}\;\text{is}\;\texttt{str}$$

The type is decided by where the value came from, not by what it looks like. Nothing inspects the characters on the way in.

$$\texttt{answer * 3}\;=\;\texttt{777}$$

Text repeated three times, which is the visible proof: a number would have given 21.

Answer $$\boxed{\text{False: always }\texttt{str}}$$
Check

The counter-test settles it: multiplying by 3 gives 777 rather than 21, and 777 is only possible if the value was text.

2§02.3 — what a method call leaves behind

A program reads a code that may have spaces around it and wants to compare it with something else. The line s.strip() appears on its own.

Find(a) True or false, with the reason.
Given
  • The claim: after the line s.strip() has run, s no longer has spaces at its two ends.

  • s holds two spaces, then 06, then two spaces.

Hint 1/4

The question is about what survives the line, so ask what the call produces and what happens to the thing it produced.

Hint 2/4

Every string operation hands back a new value and leaves its input alone; a call whose result is not assigned, printed or tested cannot change anything.

Hint 3/4

Here the result of s.strip() is not assigned to anything, and s still holds two spaces, 06, and two more spaces.

Hint 4/4

The claim is false; s = s.strip() is what the line was meant to be.

Show solution

Ask what the call produces and where it goes

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

Produces a new string. The only thing the line does with it is finish, so nothing keeps a reference to it.

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

Guaranteed, not accidental: a string cannot be edited in place at all, so the original characters are safe by construction.

Fix it in the smallest way

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

The same call with the name moved onto the result. Now the old string has no name and the trimmed one does.

Answer $$\boxed{\text{False: }\texttt{s}\text{ is untouched}}$$
Check

The printed brackets are the check: [ 06 ] before and [06] only after the assignment. Printing without the brackets would make the two lines look identical.

3§02.6 — which range gives exactly four values

Four range calls are offered. Exactly one of them makes a for loop run its body four times. No code has to be written; each call can be settled by listing its values.

Find(a) Pick the call whose loop body runs exactly four times.
Given
  • Each call is used directly as for v in <call>:.

  • A range produces start, start plus step, and so on, while the value stays on the near side of stop.

Hint 1/4

Nothing here needs a program. Four calls, four short lists, and the answer is whichever list has four members.

Hint 2/4

Write out start, start plus step, start plus two steps and so on, stopping as soon as the value is no longer on the near side of stop. Remember that stop itself never appears.

Hint 3/4

The four calls are range(0, 9, 3), range(1, 5), range(8, 0, -3) and range(2, 11, 2); two of them count downwards or in twos.

Hint 4/4

The one with four values is range(1, 5).

Show solution

List the values rather than reasoning about counts

$$\texttt{range(0, 9, 3)}\Rightarrow 0, 3, 6$$

The next would be 9, which is not below the stop, so three values.

$$\texttt{range(1, 5)}\Rightarrow 1, 2, 3, 4$$

Step 1 by default, stop excluded, so four values: this is the one.

$$\texttt{range(8, 0, -3)}\Rightarrow 8, 5, 2$$

Downwards, stopping before 0, so three values; the -1 would be needed to reach 0.

$$\texttt{range(2, 11, 2)}\Rightarrow 2, 4, 6, 8, 10$$

Five values, because 10 is still below 11.

Answer $$\boxed{\texttt{range(1, 5)}}$$
Check

For a step of 1 the count is stop minus start, which is 4 here, and that shortcut agrees with the listing. For the other three the same shortcut has to be divided by the step, which is where the arithmetic goes wrong if you skip the listing.

4§02.7 — how far a break reaches

A program searches every pair of positions with one loop inside another and stops as soon as it finds what it wants.

Find(a) True or false, with the reason.
Given
  • The claim: a break in the inner loop ends both loops, because the search is finished.

  • The outer loop has three passes still to come when the break fires.

Hint 1/4

The question is about reach, so ask which loop the break statement is inside and what the very next statement after that loop is.

Hint 2/4

A break ends the innermost enclosing loop. The statement after that loop is still in the body of the outer one, so the outer loop continues normally.

Hint 3/4

Here the break is in the inner loop and the outer loop has three passes in total, each of which finds a qualifying pair and writes over the previous answer.

Hint 4/4

The claim is false; leaving both loops needs a second test and break after the inner loop.

Show solution

Say exactly what the break ends

$$\texttt{break}\;\text{inside the inner}\;\texttt{for j}$$

It ends that loop only. Control goes to the first statement after the inner loop, which is still inside the outer one.

$$\texttt{for i}\;\text{continues}$$

So the outer loop is unaffected and runs its remaining passes as if nothing had happened.

Show the damage

$$\texttt{found = 02}\;\text{then}\;\texttt{11}\;\text{then}\;\texttt{20}$$

Each outer pass finds a qualifying pair and overwrites the record, so the printed answer is 20.

$$\texttt{if found: break}\;\text{after the inner loop}$$

The line that would actually stop the outer loop, and it has to be written separately.

Answer $$\boxed{\text{False: only the inner loop ends}}$$
Check

The run is the check: if the claim were true the program would print 02, the first find. It prints 20.

B · computation 6 questions
1§02.2 — a while loop walking a string backwards

The loop below starts at the right-hand end of a word and moves left in steps of three, building a second string as it goes. Nothing is typed in while it runs.

s = 'branching'
i = len(s) - 1
out = ''
while i >= 0:
    if s[i] in 'aeiou':
        out = out + s[i].upper()
    else:
        out = out + '.'
    i = i - 3
print(out)
print(len(out))
Find
  1. (a) List the positions the loop visits.

  2. (b) Write the two printed lines.

Given
  • The word is branching, nine characters, positions 0 to 8.

  • i starts at len(s) - 1 and decreases by 3 each pass.

  • The five vowels are a, e, i, o and u.

Hint 1/4

Do not start with the vowel test. Settle which positions the loop visits first, because there are only a few and everything else follows from them.

Hint 2/4

The loop is a countdown: i begins at len(s) - 1 and the body ends with i = i - 3, so the visited positions are that start and every third one below it, while i >= 0 holds.

Hint 3/4

With s = 'branching' the start is 8, and the word is b-r-a-n-c-h-i-n-g at positions 0 to 8. Each pass adds exactly one character to out.

Hint 4/4

The printed lines are ..A and then 3.

Show solution

Settle the positions before touching the body

$$\texttt{i = 8, 5, 2}$$

Starting at 9 minus 1 and stepping down by 3. The next would be -1, which fails the test i >= 0, so three passes.

$$\texttt{s[8] = g, s[5] = h, s[2] = a}$$

Writing the word out with positions under it once is faster than counting three times.

Run the test on each of the three characters

$$\texttt{g in 'aeiou'}\;\text{is}\;\texttt{False}$$

So the else branch adds a dot.

$$\texttt{h in 'aeiou'}\;\text{is}\;\texttt{False}$$

Another dot, giving two so far.

$$\texttt{a in 'aeiou'}\;\text{is}\;\texttt{True}$$

So a.upper() is added, which is A, and the string is now two dots and a capital A.

Report both lines

$$\texttt{print(out)}$$

One argument, so nothing extra is inserted: three characters.

$$\texttt{print(len(out))}$$

Three additions happened, one per pass, so the length must equal the number of passes.

Answer $$\boxed{\texttt{..A}\;\text{then}\;\texttt{3}}$$
Check

The two printed lines check each other: the length must equal the number of passes, because exactly one character is added per pass whichever branch runs. Three passes, three characters.

2§02.7 — a whose bound is changed in the body

Two loops, and the body changes the name that both range calls were built from. Nothing is typed in while the program runs.

rows = 3
for i in range(rows):
    for j in range(i, rows):
        print(i, j, end='  ')
    rows = rows - 1
    print()
print('rows ended at', rows)
Find
  1. (a) Say which values i takes and why.

  2. (b) Write the printed output exactly, including any blank line.

Given
  • rows is 3 when the outer for statement begins.

  • print(i, j, end=' ') leaves the line open; the print() at the bottom of the outer body closes it.

  • rows = rows - 1 runs once per outer pass, after the inner loop has finished.

Hint 1/4

Two ranges are built from the same name at different moments. Before tracing anything, decide for each of them when it is built and therefore which value of rows it sees.

Hint 2/4

A range call is worked out once, when its for statement begins. The outer one is therefore fixed for the whole program, while the inner one is built afresh at the start of every outer pass.

Hint 3/4

rows is 3 at the start and loses one at the end of each outer pass, so the inner loop sees 3, then 2, then 1 while i is 0, 1, 2.

Hint 4/4

Four lines are printed: 0 0 0 1 0 2, then 1 1, then an empty line, then rows ended at 0.

Show solution

Separate the two ranges by when they are built

$$\texttt{range(rows)}\;\text{with}\;\texttt{rows = 3}$$

Built once, when the outer for begins, so i runs 0, 1, 2 whatever happens to rows afterwards.

$$\texttt{range(i, rows)}$$

Built again at the start of each inner loop, so it sees whatever rows holds at that moment.

Walk the three outer passes

$$\texttt{i = 0: range(0, 3)}$$

Three inner passes, writing 0 0, 0 1, 0 2 with two spaces after each; then rows becomes 2 and the line closes.

$$\texttt{i = 1: range(1, 2)}$$

One inner pass, writing 1 1; then rows becomes 1 and the line closes.

$$\texttt{i = 2: range(2, 1)}$$

Empty, so nothing is written; rows becomes 0 and the print() still runs, which is where the blank line comes from.

Report the surviving value

$$\texttt{rows = 0}$$

Three outer passes, one decrease each, from 3.

Answer $$\boxed{\texttt{0 0 0 1 0 2}\;/\;\texttt{1 1}\;/\;\text{blank}\;/\;\texttt{rows ended at 0}}$$
Check

Count the printed pairs against the ranges: 3 plus 1 plus 0 is 4 pairs, and four pairs appear. Count the lines against the outer passes: one closing print per outer pass plus the final message, so four lines, one of which is empty.

An outer range is a snapshot; an inner range is rebuilt. That single asymmetry is what this question is for, and it is on the course's own paper in this exact shape.

3§02.4 — a chain with three branches inside a counted loop

Every value of a range is sorted into one of three branches by its remainder on division by 3, and one branch also adds to a total. Nothing is typed in.

total = 0
for n in range(1, 20, 4):
    if n % 3 == 0:
        total = total + n
        print(n, 'counted')
    elif n % 3 == 1:
        print(n, 'skipped')
    else:
        print(n, 'ignored')
print('total =', total)
Find
  1. (a) List the values the loop visits.

  2. (b) Write the exact output, all six lines.

Given
  • The range is range(1, 20, 4).

  • Only the first branch adds to total.

  • A remainder on division by 3 can only be 0, 1 or 2, so the else branch is the case of remainder 2.

Hint 1/4

The chain has three branches and the loop has five passes, so the work is five decisions. Get the five values down first.

Hint 2/4

range(1, 20, 4) gives start, start plus step and so on while the value stays below 20; then each value is classified by n % 3, which can only be 0, 1 or 2.

Hint 3/4

The values are 1, 5, 9, 13 and 17. Their remainders on division by 3 are 1, 2, 0, 1 and 2, and only the remainder of 0 adds to the total.

Hint 4/4

Six lines are printed, ending with total = 9.

Show solution

List the values, then their remainders

$$\texttt{1, 5, 9, 13, 17}$$

Start 1, step 4, and 21 would be past the stop of 20, so five passes.

$$\texttt{1, 2, 0, 1, 2}$$

The remainders in the same order. Doing all five at once keeps the chain from being re-read five times.

Map each remainder to its branch

$$\texttt{rem 0}\;\rightarrow\;\texttt{counted}$$

Only 9 lands here, so the total is 9 and the word counted appears once.

$$\texttt{rem 1}\;\rightarrow\;\texttt{skipped}$$

1 and 13, so that word appears twice.

$$\texttt{rem 2}\;\rightarrow\;\texttt{ignored}$$

5 and 17, in the else branch, because a remainder can only be 0, 1 or 2 and the first two are taken.

Assemble the output in loop order

$$\texttt{print(n, 'skipped')}$$

Two arguments, so one space between the number and the word; the lines come out in the order the values were visited, not grouped by branch.

$$\texttt{print('total =', total)}$$

Outside the loop, so once, and last.

Answer $$\boxed{\texttt{total = 9},\;\text{six lines in all}}$$
Check

Check the line count independently: one line per pass plus the total, so 5 plus 1 is 6. And check the total by remainder: of the five values only 9 is a multiple of 3.

4§02.3 — counting the digits and letters of a plate

Write a program, Sec02_Q1.py, that reads a vehicle plate as text and reports how many of its characters are digits and how many are letters, then says which of the two is more common, or that they are equal. Characters that are neither, such as spaces and punctuation, count as neither.

Use only a loop, a chain and the string tests from this section.

Find
  1. (a) Write the program.

  2. (b) Give the Sample Run for each of the two plates above.

Given
  • The plate is typed as one line and may contain spaces and punctuation.

  • ch.isdigit() is True only for a digit, ch.isalpha() only for a letter, and both are False for a space.

  • Two runs are wanted: 06ABC123, and 16 TR 34! with its spaces and exclamation mark.

Hint 1/4

Read the wording for how many separate answers are wanted. There are three: a count, another count, and a comparison, and the comparison needs the other two to be finished first.

Hint 2/4

Walk the characters with for ch in plate: and classify each one with ch.isdigit() and ch.isalpha(), which already hand back True or False. Since a character cannot be both, the two tests belong in one chain.

Hint 3/4

Set both counters to 0 above the loop, update exactly one of them per character, and then use a second chain with an else for the report. For 06ABC123 the answer is 5 and 3.

Hint 4/4

The program prints 06ABC123 has 5 digits and 3 letters and then More digits than letters.

Show solution

Two counters, one walk

$$\texttt{digits = 0},\;\texttt{letters = 0}$$

Both above the loop. Two separate questions about the same walk means two accumulators, not two loops.

$$\texttt{for ch in plate:}$$

The characters are all that is needed; no position is ever reported, so the shorter loop form is the right one.

Use a chain, not two ifs

$$\texttt{if ch.isdigit(): ... elif ch.isalpha(): ...}$$

A character cannot be both, so the cases are alternatives and the chain says so. It also makes the neither case free: no else is needed, and anything that fails both tests falls through untouched.

$$\texttt{no == True}$$

Both tests already hand back True or False, so comparing them with True would add a step that can only agree with its input.

Report with a second chain

$$\texttt{if digits > letters: ... elif letters > digits: ... else:}$$

Three outcomes and exactly one must print, so this is a chain with an else rather than three ifs. The else is the equal case and is easy to forget.

$$\texttt{06ABC123}\;\rightarrow\;5, 3$$

Five digits, 0, 6, 1, 2, 3, and three letters.

Answer $$\boxed{\texttt{5 digits and 3 letters},\;\text{then}\;\texttt{4 digits and 2 letters}}$$
Check

Check the two counts against the length: for 06ABC123 the total is 8 and 5 plus 3 is 8, so nothing was missed. For 16 TR 34! the length is 9 but 4 plus 2 is 6, and the missing 3 are exactly the two spaces and the exclamation mark.

When the counters are supposed to account for every character, add them up and compare with the length; when they are not, the gap should equal the characters you deliberately ignored. Either way it is a one-line check.

5§02.5 — the largest of an unknown number of typed values

Write a program, Sec02_Q2.py, that reads whole numbers one at a time until a negative number is typed, then reports how many numbers were entered and the largest of them. The negative number is the stop signal and is not one of the values. Say something sensible if the very first thing typed is negative.

Zero is a legal value and must be counted.

Find
  1. (a) Write the program.

  2. (b) Give the Sample Run for each of the two runs above.

  3. (c) Say in one sentence why the largest cannot be started at 0.

Given
  • Numbers are typed one per prompt; the first negative one stops the reading.

  • The run wanted is 4, 19, 0, 7, then -1.

  • A second run is wanted where the first thing typed is -5.

Hint 1/4

Three things have to survive the loop here: a count, a largest, and the value being tested. Work out where each of the three is created before writing the loop.

Hint 2/4

This is the read-until-stop shape: read once above the loop, loop while the value is not the stop signal, use the value and then read the next one as the last line of the body.

Hint 3/4

The stop signal is any negative number, so the condition is while value >= 0:. The inputs are 4, 19, 0, 7 and then -1, and 0 counts as a value.

Hint 4/4

It prints You entered 4 numbers and then The largest was 19.

Show solution

Lay out the read-until-stop shape first

$$\texttt{value = int(input(...))}\;\text{before the loop}$$

The condition reads value, so it must exist; this is also what makes the immediate -5 run work.

$$\texttt{while value >= 0:}$$

The stop test is negative, so the loop condition is not negative. Writing != -1 would only stop on that one number and would treat -2 as a value.

$$\texttt{value = int(input(...))}\;\text{last in the body}$$

So the value being used has already passed the test and the stop signal is never counted.

Make the first value the holder

$$\texttt{if count == 1: largest = value}$$

The first value taken in becomes the holder whatever it is. This is the part that would break if largest were started at 0 and every input were negative.

$$\texttt{elif value > largest: largest = value}$$

Every later value only has to beat the current holder. An if instead of elif here would still work, but the chain says the two cases are alternatives.

Guard the report

$$\texttt{if count == 0:}$$

With nothing entered there is no largest to print, so the program prints a sentence instead of a name that was never given a value.

$$\texttt{4, 19, 0, 7}$$

Four values, and 19 is the largest; the 0 is counted, which is why the count is 4 and not 3.

Answer $$\boxed{\texttt{You entered 4 numbers},\;\texttt{The largest was 19}}$$
Check

Two checks. Reorder the inputs: any order of 4, 19, 0, 7 must still report 19, and a program that started the holder at 0 would agree here, so the second check is the one that matters: a run of only negative values should report nothing was entered, and it does.

Two rules come out of this. The stop test and the loop condition are opposites of each other, so write one and negate it rather than inventing both. And a running largest starts from the first real value, never from zero.

6§02.6 — passes and last value of one range call

A loop is written over a single range call whose start is negative. No output is wanted from the loop itself, only two facts about it.

n = 0
last = None
for v in range(-4, 20, 6):
    n = n + 1
    last = v
print(n, last)
Find
  1. (a) How many times does the body run?

  2. (b) What is the last value the loop variable takes?

  3. (c) What would change if the stop were 21 instead of 20?

Given
  • The call is range(-4, 20, 6).

  • n counts the passes and last keeps the most recent value.

IPython console
Hint 1/4

Two facts are wanted and both come from the same short list, so write the list before answering either part.

Hint 2/4

A range gives start, start plus step and so on while the value is still below the stop; the stop itself never appears, and that holds for negative starts too.

Hint 3/4

Here start is -4, step is 6 and stop is 20, so the run begins -4, 2, 8, 14 and the next candidate is exactly 20.

Hint 4/4

The body runs 4 times and the last value is 14; with a stop of 21 it would be 5 times and 20.

Show solution

Write the run out from the negative start

$$\texttt{-4, 2, 8, 14}$$

Add 6 each time. Negative starts are only awkward on the first step, from -4 to 2, and after that it is ordinary counting.

$$\texttt{20}\;\text{is not}\;<20$$

The exclusion is exactly at the boundary here, which is the point of the question.

Answer the what-if without re-running anything

$$\texttt{range(-4, 21, 6)}$$

The same start and step, so the same run plus whatever the wider stop lets in, which is 20 and nothing else, since 26 is well past 21.

$$\texttt{5}\;\text{passes, last}\;20$$

One more pass and a different last value, from a change of one character.

Answer $$\boxed{4\;\text{passes, last}\;14;\;\text{with stop }21:\;5\;\text{and}\;20}$$
Check

Check the count by arithmetic: the distance from -4 to 20 is 24, and 24 divided by the step of 6 is exactly 4, so with the stop excluded there are 4 passes. The exact division is the signal that the stop is on a step boundary, which is when the off-by-one question actually bites.

C · exam level 4 questions
1§02.2 — splitting a code into letters and digits

A short code is walked once and its characters are sorted into two strings, which are then printed joined together, followed by one arithmetic line. Nothing is typed in.

code = 'AB3C4'
i = 0
digits = ''
letters = ''
while i < len(code):
    if code[i].isdigit():
        digits = digits + code[i]
    else:
        letters = letters + code[i].lower()
    i = i + 1
print(letters + digits)
print(int(digits) * 2)
Find(a) Pick the pair of printed lines.
Given
  • The code is AB3C4, five characters.

  • digits and letters both start as the empty string.

  • The last line converts the collected digits and doubles them.

Hint 1/4

Two printed lines, and they are independent of each other: one is about the order and case of what was collected, the other is about a conversion. Settle the two collected strings first.

Hint 2/4

Each character goes into exactly one of the two strings, in the order met, and the else branch lowers what it adds. The final arithmetic converts the whole collected digit text at once.

Hint 3/4

The code is AB3C4, so the letters are A, B, C and the digits are 3 and 4; the print joins the letters first.

Hint 4/4

The two lines are abc34 and 68.

Show solution

Walk the five characters once

$$\texttt{A, B, C}\;\rightarrow\;\texttt{letters}$$

Each is lowered as it is added, because code[i].lower() is inside the else branch, so the collected string is abc and not ABC.

$$\texttt{3, 4}\;\rightarrow\;\texttt{digits}$$

Added as characters, not as numbers, so digits holds the two-character text 34.

Read the two printed lines in order

$$\texttt{print(letters + digits)}$$

Letters first because that is the order written in the print, not the order the characters appeared in the code.

$$\texttt{int(digits) * 2}\;=\;\texttt{68}$$

The conversion happens on the joined text 34, so the arithmetic is 34 times 2 and not 3 and 4 doubled separately.

Answer $$\boxed{\texttt{abc34}\;\text{then}\;\texttt{68}}$$
Check

Check the digits both ways: as characters they are 34, and 34 doubled is 68, while doubling them one at a time would have given 6 and 8. The printed 68 rules that reading out.

2§02.4 — the digit sum of an eight digit number

Write a program, Sec02_Q3.py, that reads an eight digit number as text, refuses anything that is not exactly eight characters long, and otherwise prints the sum of its digits and whether that sum is divisible by 3.

Read the value as text rather than as a number: the digits have to be taken out one at a time, and a leading zero must survive.

Find
  1. (a) Write the program.

  2. (b) Give the Sample Run for each of the two runs above.

  3. (c) Say why the value is read as text and not with int(input(...)).

Given
  • The value is typed as one line; the run wanted is 21902456.

  • A second run is wanted with 123, which is too short.

  • Each character is a digit and can be converted on its own with int.

Hint 1/4

Two things are being asked of the input and they are not the same: how long it is, and what its characters add up to. Decide which of the two has to be checked first.

Hint 2/4

Read the value as text so that len can be used on it and its characters can be taken one at a time; then a loop with int(student[i]) builds the sum, and % 3 == 0 tests the result.

Hint 3/4

The length test comes first and everything else goes in its else branch. For 21902456 the eight digits are 2, 1, 9, 0, 2, 4, 5 and 6.

Hint 4/4

It prints The digits add up to 29 and then 29 is not divisible by 3.

Show solution

Check the length before anything else

$$\texttt{if len(student) != 8:}$$

The refusal comes first so that the rest of the program can assume eight characters. Putting the check after the loop would mean the loop had already run on bad input.

$$\texttt{else:}$$

Everything that depends on the length being right is indented under the else, which is what keeps the two cases from printing over each other.

Walk positions, convert one character at a time

$$\texttt{for i in range(len(student)):}$$

A position loop is used here because the string is being read by index; for ch in student: would work just as well and is shorter, and the position form is kept only to match the way the exam paper writes this.

$$\texttt{total = total + int(student[i])}$$

int on a single character gives its value, so the running total is a sum of digits, not a .

Test divisibility on the sum

$$\texttt{total \% 3 == 0}$$

Divisibility is always a remainder test in this course. Here 29 leaves 2, so the second message prints.

$$\texttt{2+1+9+0+2+4+5+6 = 29}$$

Adding the eight digits by hand confirms the printed total.

Answer $$\boxed{\texttt{The digits add up to 29},\;\texttt{29 is not divisible by 3}}$$
Check

Check the digit sum against the number itself: a number is divisible by 3 exactly when its digit sum is, and 21902456 divided by 3 leaves 2, the same remainder as 29 does. Two routes, one answer.

Reading a number as text is the right move whenever its digits, its length or its leading zeros matter. It also means every digit has to be converted back one at a time, which is where int(s[i]) earns its place.

3§02.5 — a loop that reads one character too far

A program is supposed to count how many times a word has two equal characters side by side. On tessera the answer is 1, the double s. The version below stops with an error instead. Its steps are numbered.

word = 'tessera'
i = 0
runs = 0
while i < len(word):
    if word[i] == word[i + 1]:
        runs = runs + 1
    i = i + 1
print(runs)
IndexError: string index out of range

Step 1 sets i to 0 and runs to 0. Step 2 is the while header. Step 3 is the comparison word[i] == word[i + 1]. Step 4 is runs = runs + 1. Step 5 is i = i + 1.

Find
  1. (a) Pick the step that has to change.

  2. (b) Say what it should be, and what the program then prints.

Given
  • The word is tessera, seven characters, positions 0 to 6.

  • The comparison reads two positions, i and i + 1.

  • The error message names an index that is out of range.

Hint 1/4

The program stops rather than printing something wrong, so the error message is evidence. Read it and ask which expressions in the program can produce it.

Hint 2/4

Whenever a loop body reads s[i + 1], the largest i the loop may reach is len(s) - 2, so the condition has to stop one earlier than a plain walk would.

Hint 3/4

Here the word has seven characters, so positions run 0 to 6, and the condition i < len(word) lets i reach 6 while the body then asks for position 7.

Hint 4/4

Step 2 is wrong and should read while i < len(word) - 1:, after which the program prints 1.

Show solution

Let the error name the guilty line

$$\texttt{IndexError}$$

The message says a string index is out of range, so some expression asked for a position that does not exist; the only expressions that index are in step 3.

$$\texttt{word[i + 1]}\;\text{with}\;\texttt{i = 6}$$

Position 7 on a seven-character word. So the largest i the loop is allowed to reach is one too big.

Fix the condition, not the comparison

$$\texttt{while i < len(word) - 1:}$$

The condition is what decides how far i goes, so that is where the limit belongs. Changing step 3 instead, by testing i + 1 < len(word) inside the body, would work but leaves the loop running a pass that does nothing.

$$\texttt{i \textbackslash{}leq 5}$$

Now the last pass reads positions 5 and 6, which is exactly the last neighbouring pair.

Run the fixed version

$$\texttt{t e s s e r a}$$

The only neighbouring pair that matches is s and s, at positions 2 and 3.

$$\texttt{runs = 1}$$

And the extra i = i + 1 inside the match branch skips past the pair so that a run of three would not be counted twice.

Answer $$\boxed{\text{step }2:\;\texttt{while i < len(word) - 1:},\;\text{prints}\;1}$$
Check

Check the fix at the boundary rather than in the middle: on a two-character word the loop must run exactly once, and i < 2 - 1 allows only i = 0, which reads positions 0 and 1. On a one-character word it must not run at all, and i < 0 refuses.

4§02.7 — two loops, two breaks, and an outer stop

A nested search prints a pair each time it finds the target, breaks out of the inner loop, and also stops the outer loop on a particular value. Nothing is typed in.

word = 'ankara'
target = 'a'
for i in range(len(word)):
    for j in range(i, len(word)):
        if word[j] == target:
            print(i, j)
            break
    if i == 2:
        break
print('stopped')
Find
  1. (a) Write the exact output.

  2. (b) Say why no line begins with 3.

Given
  • The word is ankara, six characters, positions 0 to 5, with a at positions 0, 3 and 5.

  • The inner loop starts at i, not at 0.

  • The outer loop is stopped by its own test after the inner loop, when i is 2.

IPython console
Hint 1/4

Three things interact here: where the inner loop starts, where the inner break fires, and where the outer break fires. Take one outer pass at a time and finish it before starting the next.

Hint 2/4

The inner loop runs from i to the end of the word, so its first candidate moves right as i grows. A break ends only the inner loop; the outer loop needs its own test after it.

Hint 3/4

The word is ankara with a at positions 0, 3 and 5, and the outer loop is stopped at the end of the pass where i is 2.

Hint 4/4

Four lines are printed: 0 0, 1 3, 2 3, then stopped.

Show solution

Take the outer passes one at a time

$$\texttt{i = 0}$$

The inner loop starts at 0 and finds a immediately at position 0, prints 0 0 and breaks. The outer test i == 2 fails, so the outer loop continues.

$$\texttt{i = 1}$$

The inner loop starts at 1, so it steps over the a at 0 entirely; n and k do not match, and position 3 does, so it prints 1 3.

$$\texttt{i = 2}$$

The inner loop starts at 2 and again the first match is at position 3, so it prints 2 3.

Stop the outer loop deliberately

$$\texttt{if i == 2: break}$$

Indented under the outer loop and after the inner one, so it runs at the end of that pass and ends the outer loop there.

$$\texttt{print('stopped')}$$

Outside both loops, so it runs once whichever way the outer loop ended.

Answer $$\boxed{\texttt{0 0}\;/\;\texttt{1 3}\;/\;\texttt{2 3}\;/\;\texttt{stopped}}$$
Check

Check the second numbers against the positions of a: they can only ever be 0, 3 or 5, and a line whose second number was 1, 2 or 4 would be impossible. All three printed lines pass that test.

The inner loop starting at i rather than at 0 is what makes the second numbers climb. That pattern, an inner loop beginning where the outer one is, is how every pair is visited without visiting any pair twice.

D · interleaved 4 questions
1§02.5 — a loop driven by floor division and a remainder test

A number is reduced to 1 by repeatedly halving it when it is even and taking one off when it is odd, and the passes are counted. Nothing is typed in.

n = 97
steps = 0
while n > 1:
    if n % 2 == 0:
        n = n // 2
    else:
        n = n - 1
    steps = steps + 1
print(n, steps)
Find
  1. (a) Write the two numbers the program prints.

  2. (b) Say which of the two operators, / or //, would break the program if they were swapped, and how.

Given
  • The starting value is 97.

  • n // 2 throws the fraction away; n % 2 is 0 for an even number and 1 for an odd one.

  • The loop ends when n reaches 1.

IPython console
Hint 1/4

The two printed numbers are the final value and a count, so the trace only needs one column for the value and a tally beside it.

Hint 2/4

Each pass does exactly one of two things: n // 2 if n is even, or n - 1 if it is odd. Floor division keeps the value a whole number, and the loop stops as soon as n is 1.

Hint 3/4

Starting at 97, which is odd, the values go 96, then repeated halving while they stay even, and every odd value loses one first.

Hint 4/4

It prints 1 8.

Show solution

Trace by writing only the value

$$\texttt{97}\;\text{odd}\;\rightarrow\;\texttt{96}$$

One pass. Odd numbers lose one, which always makes them even, so no two odd steps ever follow each other.

$$\texttt{96, 48, 24, 12, 6}$$

Five halvings in a row, each one pass, because each result is still even.

$$\texttt{6 \textbackslash{}rightarrow 3}\;\text{odd}\;\rightarrow\;\texttt{2}\;\rightarrow\;\texttt{1}$$

Three more passes, and then n > 1 fails.

Count the passes rather than the values

$$\texttt{1 + 5 + 3 = 8}\;\text{passes}$$

One for the first odd step, five halvings, then three more; and the list 96, 48, 24, 12, 6, 3, 2, 1 has exactly eight members, one per pass.

$$\texttt{print(n, steps)}$$

Two arguments, one space between them, so the printed line is the final value and the count.

Answer $$\boxed{\texttt{1 8}}$$
Check

Check the halvings independently: 96 is 32 times 3, so it can be halved five times before reaching 3, and 5 halvings plus 3 other passes is 8. Reaching the same 8 from the factorisation rather than from the trace is the check.

2§02.4 — a chain that picks the formatting as well as the message

A measurement is read and reported in one of three ways depending on how big it is, with a different number of decimals in each branch.

reading = float(input('Enter a measurement: '))
if reading >= 100:
    print('High: ' + format(reading, '.3f'))
elif reading >= 10:
    print('Medium: ' + format(reading, '.1f'))
else:
    print('Low: ' + str(int(reading)))
Find
  1. (a) Give the printed line for each of the two runs.

  2. (b) Say what the low branch would print for 7.99, and whether that is rounding.

Given
  • The value is typed and may have decimals.

  • Two runs are wanted: 12.3456 and 7.89.

  • format(v, '.1f') rounds to one decimal place; int(v) cuts the fraction off rather than rounding.

Hint 1/4

Two runs, two branches. For each run the first job is to find which branch the value lands in, because the formatting is chosen by the same test as the message.

Hint 2/4

The chain tests >= 100 and then >= 10, so the first true test wins and the rest are skipped. format(v, '.1f') rounds to one decimal; int(v) drops the fraction without rounding.

Hint 3/4

The values are 12.3456, which is between 10 and 100, and 7.89, which is below 10.

Hint 4/4

The two lines are Medium: 12.3 and Low: 7.

Show solution

Find the branch first, then the formatting

$$\texttt{12.3456 >= 100}\;\text{is}\;\texttt{False}$$

So the first branch is skipped; the chain moves on.

$$\texttt{12.3456 >= 10}\;\text{is}\;\texttt{True}$$

The middle branch runs, and the rest of the chain is jumped over even though the value is also above 0.

Apply the branch's own formatting

$$\texttt{format(12.3456, '.1f')}\;=\;\texttt{12.3}$$

One decimal place, and this one does round: the next digit is 4, so it rounds down.

$$\texttt{'Medium: ' + ...}\;\rightarrow\;\texttt{Medium: 12.3}$$

Joined with +, so no extra space appears beyond the one typed inside the quotes.

The second run takes a different branch

$$\texttt{7.89 >= 10}\;\text{is}\;\texttt{False}$$

So both of the first two branches fail and the else runs.

$$\texttt{int(7.89)}\;=\;\texttt{7}$$

Truncation, not rounding: the fraction is dropped whatever its size, which is why 7.99 would also give 7.

Answer $$\boxed{\texttt{Medium: 12.3}\;\text{and}\;\texttt{Low: 7}}$$
Check

Check the branch boundaries rather than the middle: a value of exactly 10 must take the middle branch, because the test is >=, and a value of 9.99 must take the low one. Testing 12 and 7 alone would not have caught a > written in place of a >=.

Two different jobs are hiding in one chain here: which message, and how many decimals. Both are decided by the same test, which is convenient and is also why a wrongly ordered chain would get both of them wrong at once.

3§02.1 — three comparisons after three conversions

Three comparisons are printed. Each mixes a converted value with an unconverted one, and one of the three does not print at all.

print(int('7') > 7)
print(str(7) > '7')
print(int('7') > '7')
Find
  1. (a) Write everything that appears on the screen, including the error.

  2. (b) Say why the second line is False rather than True.

Given
  • int('7') is the number 7 and str(7) is the one-character text 7.

  • Comparing two numbers or two strings is allowed; comparing one of each with > is not.

  • The program stops at the first line it cannot evaluate.

IPython console
Hint 1/4

Three comparisons, and the question is which of them Python is willing to make at all. So write the type of each side before working out any value.

Hint 2/4

Two numbers compare by size and two strings compare character by character, but > between a number and a string has no meaning and raises a TypeError. Lines before the failure still print.

Hint 3/4

Here int('7') is the number 7, str(7) is the text 7, and the bare 7 and '7' are a number and a text respectively.

Hint 4/4

Two lines print, both False, and then the program stops with a TypeError.

Show solution

Type both sides of each comparison before evaluating it

$$\texttt{int('7') > 7}$$

Number against number, so an ordinary comparison, and 7 is not greater than 7: False.

$$\texttt{str(7) > '7'}$$

Text against text, and the two are the same characters, so again False. This is a comparison that succeeds and returns False, which is different from one that cannot be made at all.

$$\texttt{int('7') > '7'}$$

Number against text. Nothing decides which of these is bigger, so Python refuses rather than converting one of them.

Say what reaches the screen

$$\texttt{False},\;\texttt{False}$$

The first two lines print normally, because the program runs top to bottom and only stops when it cannot continue.

$$\texttt{TypeError}$$

The third line raises it, so the two printed lines stay on screen and the third never appears.

Answer $$\boxed{\texttt{False},\;\texttt{False},\;\text{then}\;\texttt{TypeError}}$$
Check

Contrast with ==, which does have a meaning across types and would simply return False: int('7') == '7' is False, no error. So the refusal is specific to the ordering operators, not to mixing types in general.

4§02.4 — precedence inside a condition

A condition compares two arithmetic expressions, neither of which is bracketed, and then two more lines compare the two kinds of division. Nothing is typed in.

a = 2
b = 3
c = 4
if a + b * c > c ** 2 - a:
    print('left side wins')
else:
    print('right side wins')
print(a + b * c, c ** 2 - a)
print(10 / 4 == 10 // 4)
print(10 / 4, 10 // 4)
Find
  1. (a) Work out the two sides of the condition and say which branch runs.

  2. (b) Write all four printed lines.

Given
  • a is 2, b is 3 and c is 4.

  • Multiplication and the power operator bind tighter than addition and subtraction.

  • The power operator binds tighter than multiplication.

Hint 1/4

The condition is where this is decided, so evaluate its two sides separately before looking at the branches at all.

Hint 2/4

The power operator binds tightest, then multiplication and division, then addition and subtraction. So write the brackets in for yourself and evaluate inside out.

Hint 3/4

With a 2, b 3 and c 4, the two sides are 2 + (3 * 4) and (4 ** 2) - 2, and the comparison between them is a strict >.

Hint 4/4

Both sides are 14, so the else branch runs and the four lines are right side wins, 14 14, False, 2.5 2.

Show solution

Evaluate each side with the precedence written in

$$\texttt{a + b * c}\;=\;\texttt{2 + (3 * 4)}\;=\;14$$

The multiplication first, then the addition. Reading left to right instead would give 5 times 4, which is 20 and would change the branch.

$$\texttt{c ** 2 - a}\;=\;\texttt{(4 ** 2) - 2}\;=\;14$$

The power first, then the subtraction.

Settle the comparison at the boundary

$$\texttt{14 > 14}\;\text{is}\;\texttt{False}$$

A strict > on equal values is False, so the else branch runs and prints right side wins. With >= the other branch would have run, which is the whole margin here.

Read the three remaining lines

$$\texttt{print(a + b * c, c ** 2 - a)}$$

Two arguments, so one space between them: 14 14, which is the evidence for the branch above.

$$\texttt{10 / 4 == 10 // 4}$$

2.5 against 2, so False.

$$\texttt{print(10 / 4, 10 // 4)}$$

2.5 2, with the float keeping its decimal point and the floor division having none.

Answer $$\boxed{\texttt{right side wins},\;\texttt{14 14},\;\texttt{False},\;\texttt{2.5 2}}$$
Check

Check the two sides with brackets written in and nothing else changed: 2 + (3 * 4) and (4 ** 2) - 2. If either bracketing had been different the two would not both be 14 and the printed 14 14 line would have disagreed with the branch.

Mistake ledger (20 entries)
⚠ Converting on the line after, and comparing the old name

Reading and converting feel like two jobs, so they get two lines; then there are two names for the same value and the one used below is the text.

wrong$$\texttt{n = input('n: ')}\;;\;\texttt{m = int(n)}\;;\;\texttt{if n > 10:}$$
right$$\texttt{n = int(input('n: '))}\;;\;\texttt{if n > 10:}$$
⚠ Using int on text that spells a decimal

int sounds like make this a number, so it gets used for every reading, and it works until somebody types a price or a weight.

wrong$$\texttt{int(input('kg: '))}\;\text{on}\;\texttt{72.5}\;\Rightarrow\;\texttt{ValueError}$$
right$$\texttt{int(float(input('kg: ')))}\;\Rightarrow\;\texttt{72}$$
⚠ Using len(s) as a position

len feels like the last one, and in a count it is, but positions start at 0 so the last one is one less.

wrong$$\texttt{s[len(s)]}\;\Rightarrow\;\texttt{IndexError}$$
right$$\texttt{s[len(s) - 1]}\;\text{or}\;\texttt{s[-1]}$$
⚠ Expecting a slice to include its stop

Ranges in ordinary speech include both ends, so s[0:4] gets read as positions 0 to 4, which is five characters.

wrong$$\texttt{s[0:4]}\;\text{read as}\;\texttt{iter}\texttt{a}$$
right$$\texttt{s[0:4] = iter},\;\text{length}\;4-0$$
⚠ Calling a method and not keeping the answer

In some languages such a call edits the value in place, and the English reading, strip the string, suggests the same thing.

wrong$$\texttt{s.strip()}\;\text{on its own line}$$
right$$\texttt{s = s.strip()}\;\text{or}\;\texttt{clean = s.strip()}$$
⚠ Reading `in` as *made of these letters*

With a single character in really does test membership, so the habit carries over to longer pieces, where it silently starts asking about a run of neighbours instead.

wrong$$\texttt{'ak' in 'ankara'}\;\text{expected}\;\texttt{True}$$
right$$\texttt{'ak' in 'ankara'}\;\text{is}\;\texttt{False};\;\texttt{'nk' in 'ankara'}\;\text{is}\;\texttt{True}$$
⚠ Using what find returns without checking for -1

find usually succeeds while you are testing, so the failure value never appears until the marker types something that is not there.

wrong$$\texttt{s[s.find('z')]}\;\text{when z is absent}$$
right$$\texttt{p = s.find('z')};\;\texttt{if p != -1:}$$
⚠ Writing one equals sign in a condition

In ordinary mathematics one sign does both jobs, and in Python the assignment is the one people write hundreds of times a day.

wrong$$\texttt{if mark = 40:}\;\Rightarrow\;\texttt{SyntaxError}$$
right$$\texttt{if mark == 40:}$$
⚠ Writing elif where two independent tests were meant, or the reverse

Both read the same in English. If it is hot, wear a hat. If it is warm, leave the coat does not say whether the two can both apply.

wrong$$\texttt{if a: ... if b: ...}\;\text{when the cases are alternatives}$$
right$$\texttt{if a: ... elif b: ...}\;\text{for alternatives};\;\text{two ifs for independent tests}$$
⚠ Leaving the last line inside the block by accident

The editor keeps the indentation of the previous line, so the line after a block starts indented unless you move it back.

wrong$$\texttt{if mark >= 40:}\;/\;\texttt{ print('pass')}\;/\;\texttt{ print('recorded')}$$
right$$\texttt{if mark >= 40:}\;/\;\texttt{ print('pass')}\;/\;\texttt{print('recorded')}$$
⚠ The change to the counter sits outside the block

It is one line and four spaces, and the editor does not care. The program still runs, which is why this one shows up as a frozen terminal rather than as an error.

wrong$$\texttt{while c > 0:}\;/\;\texttt{ print(c)}\;/\;\texttt{c = c - 1}$$
right$$\texttt{while c > 0:}\;/\;\texttt{ print(c)}\;/\;\texttt{ c = c - 1}$$
⚠ Reading the next value at the top of the body

One read looks tidier than two, and the loop does finish, so the program looks like it works until the total is checked by hand.

wrong$$\texttt{while v != 0:}\;/\;\texttt{ v = int(input())}\;/\;\texttt{ total += v}$$
right$$\texttt{while v != 0:}\;/\;\texttt{ total += v}\;/\;\texttt{ v = int(input())}$$
⚠ Testing a float for equality in the condition

Adding 0.1 ten times obviously reaches 1.0 on paper, and the loop that never stops looks like a hang rather than a wrong answer.

wrong$$\texttt{while x != 1.0:}\;/\;\texttt{ x = x + 0.1}$$
right$$\texttt{while x < 1.0:}\;\text{or count whole steps}$$
⚠ Writing the stop as the last value you want

From 1 to 10 includes 10 in every other context, so the exclusion has to be learned rather than guessed.

wrong$$\texttt{range(1, 10)}\;\text{for}\;1\dots 10$$
right$$\texttt{range(1, 11)}\;\text{for}\;1\dots 10$$
⚠ A countdown that stops one short of zero

With a negative step the stop still excludes itself, so the value you want to reach has to be written one further out, which looks like a typing mistake.

wrong$$\texttt{range(5, 0, -1)}\;\Rightarrow\;5,4,3,2,1$$
right$$\texttt{range(5, -1, -1)}\;\Rightarrow\;5,4,3,2,1,0$$
⚠ Changing the bound inside the loop and expecting the loop to notice

The call looks like it is re-read each pass, the way a while condition is. It is not: the values were settled when the for began.

wrong$$\texttt{for i in range(n): n = 1}\;\text{expected to stop early}$$
right$$\texttt{break}\;\text{is how a for stops early}$$
⚠ Expecting break to leave both loops

In English stop means stop, and the find really has happened, so there seems to be nothing left to do.

wrong$$\texttt{for i: for j: if hit: break}\;\text{expected to end the search}$$
right$$\texttt{for i: for j: if hit: break}\;/\;\texttt{ if hit: break}$$
⚠ Reassigning the outer bound to end the outer loop early

A while loop really does re-read its condition each pass, so the same move looks available in a for.

wrong$$\texttt{for i in range(n): n = 1}$$
right$$\texttt{for i in range(n): if done: break}$$
⚠ Buried error at step 3 of the error hunt

The range starts at 1, so position 0 is never looked at. On Bilkent that loses the B from the consonant string, which is why the program prints lknt instead of Blknt. It would also miss a vowel sitting at position 0 entirely. Counting from 1 is what everyone does when saying the first letter, and the loop runs without complaint, so the only symptom is one missing character at the front.

⚠ Buried error at step 4 of the error hunt

position = i runs for every vowel, so each one overwrites the last and the value left at the end is the position of the final vowel, not the first. On Bilkent that prints 4, the e, instead of 1, the i. The line is true of every vowel, and while testing on a word with only one vowel it gives the right answer, so the bug stays hidden until a second vowel appears.

Formula card
input always hands back text
$$\boxed{\texttt{value = input(prompt)}\;\Longrightarrow\;\texttt{type(value)}\;\text{is}\;\texttt{str}}$$

No conditions; it holds for every call to input.

one character with a number, a piece with a colon
$$\boxed{\texttt{s[i]}\;\text{is one character;}\quad \texttt{s[a:b]}\;\text{is}\;\texttt{s[a]},\ \dots,\ \texttt{s[b-1]}}$$

The bare index needs 0 <= i < len(s); the slice needs nothing and is trimmed instead.

strings are read-only, so every operation returns
$$\boxed{\texttt{t = s.lower()}\;\Longrightarrow\;\texttt{t}\;\text{is new},\;\texttt{s}\;\text{is unchanged}}$$

Holds for every string operation in this course.

the first true test in a chain wins, and the rest are skipped
$$\boxed{\texttt{if c1: ... elif c2: ... else: ...}\;\Longrightarrow\;\text{one block, the first with}\;\texttt{ci}\;\text{true}}$$

The chain must be one statement: elif and else at the same indentation as their if.

the three parts of a loop that ends
$$\boxed{\text{set up}\;\rightarrow\;\text{test}\;\rightarrow\;\text{body}\;\rightarrow\;\text{change}\;\rightarrow\;\text{test again}}$$

Part 3 has to be inside the indented block and has to move a name the condition reads.

what a range call produces
$$\boxed{\texttt{range(a,b,c)}\;\Longrightarrow\;a,\;a+c,\;a+2c,\;\dots\;\text{while still on the near side of}\;b}$$

The step must point from start towards stop, or the range is empty and the body never runs.

the inner loop runs to the end for each single pass of the outer one
$$\boxed{\texttt{for i in ...: for j in ...: body}\;\Longrightarrow\;\text{body runs}\;(\text{outer count})\times(\text{inner count})\;\text{times}}$$

break reaches only the innermost enclosing loop; leaving two needs a second test after the inner one.

Check yourself

Close the page and write, from memory and without running anything: the type that comes back from input; the length of s[a:b]; what happens to s[len(s)] and to s[len(s):]; the three parts a while loop needs in order to end; the values of range(5, 0, -1) and of range(5, -1, -1); and which loop a break leaves. Then write one seven-line program that reads values until a zero and reports their average, and check your own output against a run.

  • Say what type input hands back, and write the two conversions that turn 72.5 typed at the keyboard into the whole number 72?

    c-input

  • Give the value of s[2:8:2], s[::-1] and s[-3:] on a word you choose, and say which of s[9] and s[9:12] stops the program?

    c-strings

  • Say what s.strip() hands back, what it changes, and what ''.isalpha() and s.find('z') give when the thing looked for is not there?

    c-string-ops

  • Write a four-test chain that maps a mark to 100, 80, 50 or 20 and explain why reversing the order makes three of the four unreachable?

    c-

  • Write the read-until-stop shape from memory with the two input calls in the right places, and say what your program prints when the stop value is the first thing typed?

    c-while

  • List the values of range(2, 12, 3) and of range(3, 10, -1) without running them, and say which range walks every position of a string?

    c-for-range

  • Trace a nested loop whose bound is reassigned in the body, and write the two statements needed to leave both loops on a find?

    c-nested-break

Glossary (29 terms)
inputgirdi

The built-in call that waits for a line to be typed and hands it back as text. Its optional argument is written to the screen first, with no line break of its own.

promptistem

The message shown to the person before their typing is read. It is only characters on the screen; it has no effect on what comes back.

casttür dönüşümü

Converting a value from one type to another with a call such as int, float or str. Nothing is converted automatically in this course, so the cast has to be written.

indexindeks

The position of a character inside a string, counted from 0 at the left end or from -1 at the right. A bare index outside the string stops the program.

slicedilim

A new string cut out of another one by giving a start and a stop, and optionally a step. The stop position is never included, so the length is stop minus start.

stepadım

The third number in a slice or a range, saying how far to jump between values. A negative step walks the other way.

değiştirilemez

A value that cannot be altered once it exists. Strings are immutable, which is why every string operation hands back a new string instead of editing the one it was given.

concatenationbirleştirme

Joining two strings with + to make a third, with nothing inserted between them. It refuses a string and a number as operands.

membership test

The question t in s, which is True when t appears inside s as a run of neighbouring characters. It is case sensitive.

sözlük sırası

The order in which strings compare: position by position, using the character codes, so that the first position where they differ decides. Capitals come before small letters.

relational operatorkarşılaştırma işleci

One of the six comparisons, from == to >=, each of which hands back True or False rather than a number.

mantıksal ifade

Anything whose value is True or False, whether it is a comparison, a test such as isdigit, or a value being read for truth, where 0 and the empty string count as False.

branchingdallanma

Choosing which statements run next by testing a condition, written with if, elif and else.

chain

One if with its elifs and else attached. At most one of its blocks runs, which is what separates it from several independent if statements.

indentationgirinti

The leading spaces that say which statements belong to the test or loop above them. In Python it is part of the program, not a matter of layout.

blockblok

The group of statements indented under a test or a loop header, all of which share its fate.

yineleme

Running the same block more than once. One run of the block is called a pass.

while loop

A loop whose condition is checked before each pass, used when the number of passes is not known before the loop begins.

for loop

A loop that takes its variable from a run of values, one per pass, used when that run is known before the loop begins.

rangearalık

A call that names a run of numbers by start, stop and step, for a for loop to walk. The stop value is never produced.

accumulator

A name created above a loop and updated inside it, so that it carries a total, a count or a built string from one pass to the next.

A value whose only job is to say stop, typed by the user rather than counted, and deliberately not one of the values being collected.

infinite loopsonsuz döngü

A loop whose condition never becomes false, usually because nothing inside the block changes the names the condition reads.

nested loopiç içe döngü

A loop written inside the body of another. The inner one runs to its end for every single pass of the outer one.

break

A statement that ends the innermost loop containing it at once, skipping the rest of that pass and the rest of that loop, but no loop further out.

off by one

A mistake in which a loop or a slice starts or stops one position away from where it should. It is the most common kind of error in this chapter and it never produces an error message.

IndexError

What stops the program when a bare index asks for a position the string does not have. A slice never raises it.

ValueError

What stops the program when a conversion is given characters it cannot read as a number, such as int of text spelling a decimal.

TypeError

What stops the program when an operator is given a pair of types it has no meaning for, such as + between text and a number.

What comes next
§03 · Simple numerical programs (Chapter 3)

Everything on this page was a tool: read a value, ask a question about it, cut a piece out of some text, do it again. The next section spends those tools on a single job, finding a number that satisfies a condition when no formula hands it to you, which is what the textbook calls a simple numerical program. The loop shapes do not change; what changes is that the loop is now searching, so the interesting question becomes how many passes it takes and whether a cleverer choice of next guess makes that number much smaller.

Sources
  • kitapJohn Guttag, Introduction to Computation and Programming Using Python, with Application to Understanding Data, second edition, chapter 2 The syllabus names this chapter for the week. The third edition is also accepted on the course and covers the same material; only the exercise numbering moves.
  • ders malzemesiThe course's own lecture slides for this week and the next Used for the boundary of what counts as covered: strings with indexing, slicing, concatenation, repetition and membership; branching with if, elif and else; then while, for, range, nested loops and break. The slides themselves credit an MIT introductory course as their source.
  • ders malzemesiThe lab sheets for the first two lab sessions Used only for the shape of an exercise, that is, a named script plus a Sample Run given character for character. No lab question is reproduced here; every exercise on this page is a different problem measuring the same skill.
  • ders malzemesiOne past midterm paper for this course, with its solutions Used for the weight and the shape of the tracing question and for the list of calls printed on the cover sheet. Measured on one paper only, so it is quoted as one observation rather than as a rule.
  • sabitThe Python 3 language reference and its library documentation Used to check the exact behaviour of the string methods, the empty-string cases in the operations table, and which error type each failure raises.

Spotted something missing or wrong? tell us · share your own notes or an old exam.

Last updated .