8b Slices, Immutability, Searching, and Counting

This lesson introduces the slice operator and the between-the-characters picture that explains it, discovers that strings cannot be changed at all, and names the two computational patterns most string processing is built from: the search and the counter.

Subject: Python · 65 slides · code lesson

Open the interactive version of this deck

What this lesson covers

The lesson, slide by slide

1. Lesson 8b Slices, Immutability, Searching, and Counting

Title

Python · Chapter 8 — Strings

§8.4-8.7, pp. 73-75

2. By the end of this lesson you can

Objectives

Five things, each one you can check yourself at an interpreter prompt.

Think Python, 2nd edition — Allen B. Downey §8.4-8.7, pp. 73-75 — the pages these objectives are drawn from

3. Before we start: how would you take three characters?

Warm-up

You can take one. Now try to take several.

Discussion prompt

With only the bracket operator from the last lesson, how would you get the first three characters of 'banana' as a single string? Write it out, and say what is unsatisfying about it.

Hint: One character at a time, joined together.

Answer:

fruit[0] + fruit[1] + fruit[2] works and gives 'ban'.

What is unsatisfying is that it hard-codes the number three. Taking the first n characters would need a loop, for something that ought to be one operation.

A segment of a string is called a slice, and Python has an operator for exactly this. It is written with a colon, and the whole of this section is about the one thing people find surprising about it.

4. The one idea behind this lesson: the indices point between the characters

Concept

The slice operator returns the part of the string from one index to another, including the first and excluding the last. That is counterintuitive as stated — and it becomes obvious if you imagine the indices pointing between the characters rather than at them.

slice — A part of a string specified by a range of indices.

This is the book's own device, and figure 8.1 draws it. Under that reading a slice is not characters n to m but everything between position n and position m, which is a length rather than a list of positions.

Figure (svg): The string banana with index markers drawn between the characters, from zero to six

Six characters, seven boundaries. A slice names two boundaries and takes what is between them.

Think Python, 2nd edition — Allen B. Downey §8.4-8.7, pp. 73-73 — figure 8.1, slice indices

5. The slice operator

Section

Section 1

6. Taking several characters at once

Concept

A segment of a string is called a slice, and selecting one is similar to selecting a character. The operator takes two indices separated by a colon.

>>> s = 'Monty Python'
>>> s[0:5]
'Monty'
>>> s[6:12]
'Python'
ExpressionWhat it takesResult
s[0:5]from boundary 0 to boundary 5five characters
s[6:12]from boundary 6 to boundary 12six characters
the rulem minus n charactersno adjustment needed

The operator returns the part of the string from the n-eth character to the m-eth character, including the first and excluding the last. This behaviour is counterintuitive, but it might help to imagine the indices pointing between the characters.

Think Python, 2nd edition — Allen B. Downey §8.4-8.7, pp. 73-73

7. Picture it: figure 8.1, the boundaries

Picture it

Six characters have seven boundaries, numbered 0 to 6.

Figure (svg): The string banana with boundary markers numbered zero through six drawn between and around the characters

A slice names a start boundary and a stop boundary, and takes everything between.

Under this reading, s[0:5] taking five characters needs no explanation at all: 5 minus 0 is 5. The half-open convention stops being a rule and becomes arithmetic.

8. Worked example: why the length is the difference

Worked example

The clean arithmetic is the whole reason for the convention.

>>> s = 'Monty Python'
>>> len(s[0:5])
5
>>> len(s[6:12])
6
>>> len(s[2:2])
0
SliceThe subtractionLength
s[0:5]5 - 05 characters
s[6:12]12 - 66 characters
s[2:2]2 - 20 characters

Compute the length as a subtraction.

Why: The number of characters in s[n:m] is m minus n, with no correction term at all.

Check it on the third case.

Why: Two minus two is zero, and indeed the slice is empty. The arithmetic keeps working at the boundary.

Notice what an inclusive convention would cost.

Why: If the end were included, the length would be m minus n plus one, and every calculation involving slice lengths would carry that plus one — which is exactly the kind of adjustment that produces off-by-one errors.

Figure (svg): Two columns comparing the half-open slice convention with a hypothetical inclusive one

The left column's arithmetic has no correction terms. That is the entire argument.

The length of a slice is always the difference of its indices. That is the payoff of excluding the end, and it is why the convention is worth the initial surprise.

Verify: Check that adjacent slices join up without overlap or gap.

Why: s[0:5] and s[5:12] together give the whole string, with no character repeated and none missing. Under an inclusive convention the second slice would have to start at 6, and forgetting that is a classic bug. Adjacent slices sharing a boundary index is a direct consequence of the half-open rule.

9. Predict: what is s[1:4]?

Prediction

Boundaries 1 to 4, which is three characters.

>>> s = 'banana'
>>> s[1:4]
MarkerWhere it isWhat is between
boundary 1just after the bstart here
boundary 4just after the second nstop here
between thema, n, a'ana'

Predict first

What is s[1:4] when s is 'banana'?

  • 'anan'
  • 'ana'
  • 'ban'
  • 'nan'

Correct: 'ana' — three characters, because 4 minus 1 is 3.

Why: The slice starts at index 1, which is the second character, and stops before index 4. The subtraction gives the length immediately: three characters. If you answered 'anan', you included the character at index 4, which is the half-open convention being read as inclusive.

10. Worked example: the omitted-index forms

Worked example

Leaving out an index means all the way to the end.

>>> fruit = 'banana'
>>> fruit[:3]
'ban'
>>> fruit[3:]
'ana'
>>> fruit[:]
'banana'
SliceWhat it meansResult
fruit[:3]from the beginning to boundary 3'ban'
fruit[3:]from boundary 3 to the end'ana'
fruit[:]from the beginning to the endthe whole string

Omit the first index.

Why: If you omit the first index, before the colon, the slice starts at the beginning of the string.

Omit the second.

Why: If you omit the second index, the slice goes to the end of the string.

Omit both.

Why: The book asks what fruit[:] means and invites you to try it. It gives the whole string — from the beginning to the end.

Figure (svg): The state of the program after each line of Worked example the omitted-index forms, drawn as a ladder with one rung per traced line

The whole run at once: each drop is one line of the program.

'ban', 'ana', and the whole string. The omitted forms are shorthand for 0 and len, and they are used constantly because they need no arithmetic.

Verify: Check that the two halves reassemble.

Why: fruit[:3] + fruit[3:] gives 'banana' exactly. Splitting at any boundary and rejoining always reproduces the original, which is another consequence of the half-open convention and a useful sanity check.

11. Trap: expecting the end index to be included

Trap

The trap

A student writes s[0:5] expecting six characters, reasoning that indices 0 through 5 is six positions.

Read the indices as naming characters

Why: Which is how the bracket operator works with a single index, so the reading carries over naturally.

The slice gives five characters, and every slice in the program is one character short of what was intended — silently.

The fix

Read the indices as boundaries between characters, not as characters.

Use the book's picture

Why: Imagine the indices pointing between the characters, as in figure 8.1. Then s[0:5] is everything between boundary 0 and boundary 5.

Check with the subtraction

Why: The length is m minus n. If you wanted six characters starting at 0, the slice is s[0:6].

The convention is genuinely counterintuitive at first and the book says so. What makes it worth learning is that every piece of slice arithmetic comes out without a correction term.

12. Match each slice to what it produces

Matching

For fruit = 'banana'.

Match the pairs

  • a. fruit[:3]
  • b. fruit[3:]
  • c. fruit[:]
  • d. fruit[2:4]
  • r1. 'ban'
  • r2. 'ana'
  • r3. 'banana'
  • r4. 'na'

Why: The first two split the string at boundary 3 and together reproduce it. The third omits both indices and gives the whole string. The fourth takes two characters, because 4 minus 2 is 2 — the subtraction gives the length in every case, which is the quickest way to check a slice before running it.

13. Complete it: take the last three characters

Faded example

Two ways to write it. Give the one that needs no length.

Fill in the blanks

word = 'banana'
last_three = word[-3:] # should be 'ana'

Why: A negative start index counts back from the end, so word[-3:] takes the last three characters whatever the string's length. The positive equivalent is word[len(word)-3:], which needs the length and a subtraction. Negative indices work in slices exactly as they do in single-character selection, and for the same reason: they save an arithmetic step where the arithmetic could go wrong.

14. Push the boundary: what if the indices are out of order?

Edge cases

The book states the answer. Predict it, then explain it.

Discussion prompt

What does fruit[3:3] produce, and what about fruit[4:2]? Explain both using the boundary picture rather than as special cases.

Hint: How much is between boundary 3 and boundary 3?

Answer:

Both give the empty string. If the first index is greater than or equal to the second, the result is an empty string, represented by two quotation marks.

The boundary picture explains it without a special rule: there is nothing between boundary 3 and boundary 3, and nothing between boundary 4 and boundary 2 going forwards.

An empty string contains no characters and has length 0, but other than that it is the same as any other string — it can be concatenated, compared and traversed, and traversing it does nothing, which is exactly right.

15. Strings are immutable

Section

Section 2

16. You cannot change a character

Concept

It is tempting to use the bracket operator on the left side of an assignment, with the intention of changing a character in a string. Python refuses.

immutable — A property of a value that cannot be modified after it is created.

>>> greeting = 'Hello, world!'
>>> greeting[0] = 'J'
TypeError: 'str' object does not support item assignment
LineWhat happensNote
greeting[0] = 'J'an attempt to change one characterTypeError
the reasonstrings are immutableyou cannot change an existing string
the alternativecreate a new stringa variation on the original

The object in this case is the string and the item is the character you tried to assign. For now, an object is the same thing as a value — the book refines that definition in chapter 10.

Think Python, 2nd edition — Allen B. Downey §8.4-8.7, pp. 74-74

17. Picture it: modify versus rebuild

Picture it

The original is never touched. A new string is made from pieces of it.

Figure (svg): Two columns contrasting an attempt to modify a string in place with building a new string from a slice

The best you can do is create a new string that is a variation on the original.

Notice that this is the same move as lesson 2b's concatenation: nothing is modified, a new value is produced. Immutability is why that was always true.

18. Worked example: building the variation

Worked example

One character replaced, without modifying anything.

>>> greeting = 'Hello, world!'
>>> new_greeting = 'J' + greeting[1:]
>>> new_greeting
'Jello, world!'
>>> greeting
'Hello, world!'
ExpressionWhat it producesResult
greeting[1:]everything from index 1 onward'ello, world!'
'J' + ...concatenate a new first charactera new string
greetingunchangedstill 'Hello, world!'

Take everything except the character being replaced.

Why: greeting[1:] is a slice from index 1 to the end, which drops the first character.

Concatenate the replacement onto the front.

Why: This example concatenates a new first letter onto a slice of greeting.

Check the original.

Why: It has no effect on the original string. Two strings now exist, and greeting still refers to the first.

Figure (svg): The state of the program after each line of Worked example building the variation, drawn as a ladder with one rung per traced line

The whole run at once: each drop is one line of the program.

A new string, 'Jello, world!', with the original untouched. Replacing a character means building a new string from the parts you want to keep.

Verify: Check that both strings still exist.

Why: greeting is unchanged and new_greeting holds the variation, so nothing was destroyed. That is the defining property: an immutable value cannot be modified, so an apparent modification must have produced something new.

19. Predict: what happens here?

Prediction

The bracket operator on the left of an assignment.

s = 'hello'
s[0] = 'H'
PartWhat it meansResult
s[0]on the LEFT of an assignmentan attempt to modify
strings are immutablemodification is not supportedTypeError
the messagestr object does not support item assignmentnames the problem

Predict first

What happens when this runs?

  • s becomes 'Hello'
  • A TypeError: str object does not support item assignment
  • A SyntaxError, because brackets cannot appear on the left
  • Nothing — the line is ignored

Correct: A TypeError saying that a str object does not support item assignment.

Why: Strings are immutable, so there is no way to change an existing one. Note that this is a TypeError rather than a SyntaxError: the line is perfectly well-formed, and it is the type of the thing being assigned to that makes the operation unavailable. Lists in chapter 10 accept exactly this syntax, which is why the error is about the type rather than about the brackets.

20. Worked example: replacing a character in the middle

Worked example

The same technique, with two slices rather than one.

>>> word = 'banana'
>>> new_word = word[:2] + 'X' + word[3:]
>>> new_word
'baXana'
PartWhat it takesResult
word[:2]everything before index 2'ba'
'X'the replacementone character
word[3:]everything after index 2'ana'

Take the part before the character.

Why: word[:2] stops at boundary 2, so it excludes the character at index 2 — which is the one being replaced.

Supply the replacement.

Why: Concatenated in the middle.

Take the part after.

Why: word[3:] starts at boundary 3, so it skips the replaced character and keeps everything after it.

Figure (svg): The string banana with the character at index two highlighted and the two surrounding slices marked

word[:2] takes what is before the highlighted character; word[3:] takes what is after.

'baXana'. The two slices are word[:i] and word[i+1:], which between them cover everything except the character at index i.

Verify: Check the boundaries by reassembling without the replacement.

Why: word[:2] + word[2:] gives 'banana' back, so the character at index 2 is exactly the one dropped when the second slice starts at 3. Testing the reassembly first is a good way to get the two indices right before adding the replacement.

21. Trap: expecting a method to change the string

Trap

The trap

A student calls a string method that returns a modified string and does not assign the result, expecting the original to have changed.

Assume an operation on a value modifies it

Why: Which is true for many things and is exactly what immutability rules out for strings.

The method returns a new string and the original is untouched, so the program continues with the unmodified value and nothing indicates why.

The fix

Every string operation produces a new string. Assign it, or it is lost.

Read every string operation as producing a value

Why: Which is lesson 6a's fruitful-function point: the result has to be used, or the call was pointless.

Assign the result, usually back to the same name

Why: word = word.upper() rather than word.upper() on its own.

This becomes the standard shape for string work: build a new string from pieces of the old one and assign it. Chapter 10's lists behave differently, and the contrast is one of the most important in the book.

22. Two truths and a lie: immutability

Two truths and a lie

Two are true. Keep the lie.

Eliminate the wrong options

Rule out the two true statements.

  • A. You cannot change a character of an existing string
  • B. You can create a new string that is a variation on the original
  • C. Immutability means a variable holding a string cannot be reassigned

Survives elimination: C

Why: C confuses two different things. The VALUE cannot be changed; the NAME can be repointed at any time. greeting = 'goodbye' is perfectly legal and creates no conflict with immutability, because it changes what the name refers to rather than changing any string. Lesson 7a's state diagram is the picture that keeps these apart: immutability is about the box on the right of the arrow, and reassignment moves the arrow.

23. Complete it: replace the last character

Faded example

Keep everything except the last character, then add the replacement.

Fill in the blanks

word = 'banana'
new = word[:-1] + 'S' # should be 'bananS'

Why: word[:-1] takes everything up to but not including the last character, which is exactly what needs keeping. The positive equivalent is word[:len(word)-1], which needs the length. Note how the half-open convention helps here: the slice stops at the boundary before the last character, so no plus-or-minus-one adjustment is needed beyond the -1 itself.

24. Think it through: why would a language make strings immutable?

Socratic

Lists are mutable. Ask what strings gain by not being.

Discussion prompt

Python's lists can be changed in place and its strings cannot. Suggest one advantage of immutability, and one thing it makes more awkward.

Hint: Think about two names referring to the same string.

Answer:

The advantage is safety from surprise: if two names refer to the same string, nothing either of them does can change what the other sees. Chapter 10 shows what happens when that guarantee is absent, and the section is called Aliasing for a reason.

It also lets Python share strings freely behind the scenes, since no one can modify a shared value.

What it makes awkward is building a string up piece by piece, since every step creates a new string. For long strings that is a real cost, and chapter 10's lists are the usual answer — build a list, then join it.

25. The search pattern

Section

Section 3

26. Traverse, and return when you find it

Concept

This pattern of computation — traversing a sequence and returning when we find what we are looking for — is called a search. The book's example is the inverse of the bracket operator.

def find(word, letter):
    index = 0
    while index < len(word):
        if word[index] == letter:
            return index
        index = index + 1
    return -1
PartIts roleNote
the traversalindex from 0 to len-1the standard pattern
the testis this the character?inside the loop
return indexfound: stop immediatelythe first return
return -1the loop finished without findingnot found

In a sense find is the inverse of the bracket operator: instead of taking an index and extracting the corresponding character, it takes a character and finds the index where that character appears. This is the first example of a return statement inside a loop.

Think Python, 2nd edition — Allen B. Downey §8.4-8.7, pp. 74-75

27. Picture it: two ways out

Picture it

A search has two exits, and each returns something different.

Figure (svg): A flow chart showing a search loop with an early return when the target is found and a return of minus one after the loop

The two exits are what make this a pattern rather than an ordinary traversal. An ordinary traversal always visits everything; a search stops as soon as it has an answer.

28. Worked example: tracing a successful search

Worked example

It stops as soon as it finds the character. Count the passes.

>>> find('banana', 'n')
2
PassThe testWhat happens
index 0'b' == 'n'? noadvance
index 1'a' == 'n'? noadvance
index 2'n' == 'n'? yesreturn 2 immediately
indices 3-5never examinedthe function has already returned

Traverse from the start.

Why: The standard index traversal from the previous lesson, unchanged.

Return as soon as the test succeeds.

Why: If the character matches, the function breaks out of the loop and returns immediately — which is what the return statement does, from lesson 6a.

Notice what is skipped.

Why: Indices 3, 4 and 5 are never examined. A search does the minimum work needed to answer the question.

Figure (svg): The state of the program after each line of Worked example tracing a successful search, drawn as a ladder with one rung per traced line

The whole run at once: each drop is one line of the program.

2, after three passes. The second 'n' at index 4 is never reached, because find returns the position of the FIRST occurrence.

Verify: Search for a character that appears twice and check which position comes back.

Why: find('banana', 'a') gives 1, not 3 or 5. That the first occurrence wins is a consequence of returning immediately, and it is worth knowing because a search that needed the LAST occurrence would have to be written differently.

29. Predict: which index comes back?

Prediction

The character appears more than once.

>>> find('banana', 'a')
PassWhat happensNote
index 0'b', no matchadvance
index 1'a', matchreturn 1
indices 3 and 5also 'a'never reached

Predict first

What does find('banana', 'a') return?

  • 1
  • 3
  • 5
  • All three

Correct: 1 — the first occurrence, because the function returns immediately on finding a match.

Why: The a's are at indices 1, 3 and 5, and the search stops at the first. This is a consequence of the early return rather than a special rule, and it is worth being explicit about: a search returns the FIRST match. Finding the last one would need a different structure — traversing the whole string and remembering the most recent match.

30. Worked example: the not-found case

Worked example

The second exit, and why it is after the loop rather than inside it.

>>> find('banana', 'z')
-1
StageWhat happensNote
indices 0 to 5no match at anythe loop completes
after the loopreturn -1the not-found value
why afteryou only know it is absent once you have checked allthe last possible moment

Let the loop finish.

Why: If the character does not appear in the string, the program exits the loop normally.

Return the sentinel.

Why: It returns -1, which is a value no valid index can be — so the caller can distinguish it from a real answer.

Notice why it cannot be inside the loop.

Why: You cannot conclude a character is absent until every position has been checked, so the not-found return must come after the traversal, not during it.

Figure (svg): Two columns comparing a successful search that returns early with an unsuccessful one that returns after the loop

Two exits, two kinds of answer, and one of them can only be known at the end.

-1, returned after the loop has examined every character without a match. The placement is forced: absence can only be established once the search is complete.

Verify: Check that -1 could not be a genuine answer.

Why: Valid indices are 0 and above, so -1 is unambiguous as a not-found marker — which is what makes it a usable sentinel. Compare with lesson 6c's boundary probe, where None was a bad sentinel precisely because it could also be a real result.

31. Trap: returning not-found inside the loop

Trap

The trap

A student adds an else to the if, returning -1 when the current character does not match.

Treat each character as decisive

Why: It feels symmetric: match means found, no match means not found.

Now the function returns -1 after examining only the first character. It reports not found for every string whose first character is not the target, including strings that contain it.

The fix

A single non-match proves nothing; only an exhausted loop does.

Return the found value from inside the loop

Why: One match is enough to answer yes.

Return the not-found value after the loop

Why: Because you only know a character is absent once you have checked every position.

This asymmetry is the essence of the search pattern, and it recurs everywhere: proving something exists takes one example, and proving it does not takes an exhaustive check.

32. Discriminate: inside the loop or after it?

Discrimination

Ask what evidence each conclusion needs.

Sort into buckets

For a search, where does each of these belong?

inside the loop
return the index when the character matches; advance to the next index; test whether the current character matches; conclude the string does contain the character
after the loop
return -1 because the character is absent; conclude the string does not contain the character
in
Each of these concerns the current character, or is settled by a single match. One example is enough to conclude the character is present, so that conclusion can be drawn inside the loop.
after
Both concern the whole string. Absence cannot be established from any single position, so it can only be concluded once every position has been checked — which is after the loop.

33. Complete it: add a starting index

Faded example

The book's exercise: let the caller say where to start looking.

Fill in the blanks

def find(word, letter, start):
index = start
while index < len(word):
if word[index] == letter:
return index
index = index + 1
return -1

Why: The only change is the initialization: instead of always beginning at 0, the traversal begins wherever the caller says. Everything else — the condition, the test, the two returns — is unchanged, which is a good sign that the search pattern is separable from where it starts. This three-parameter version is exactly what the next lesson's counting exercise uses to find repeated occurrences.

34. Explain it: why is -1 returned after the loop?

Explain it

The asymmetry is the interesting part.

Discussion prompt

A classmate wants to add an else that returns -1 when a character does not match. Explain why the two returns cannot both go inside the loop.

Hint: What does one non-matching character actually prove?

Answer:

Say: one match proves the character is there, but one non-match proves nothing at all — the character could be at any later position.

So the found-return can go inside, because a single piece of evidence settles it. The not-found return has to wait until every position has been checked, which is after the loop.

Give them the test that shows the bug: with the else in place, find('banana', 'n') returns -1, because the first character is a b. One test with the target anywhere but position zero exposes it immediately.

35. The counter pattern

Section

Section 4

36. Traverse everything and keep a tally

Concept

The second pattern of computation in this chapter is a counter: a variable initialized to zero and incremented each time something is found. Unlike a search, it always examines the whole sequence.

word = 'banana'
count = 0
for letter in word:
    if letter == 'a':
        count = count + 1
print(count)
LineIts roleNote
count = 0initialized before the loopthe starting tally
for letter in wordtraverse every characterno early exit
if letter == 'a'the conditiondecides whether to count
count = count + 1the updateonly when the condition holds

The variable count is initialized to 0 and then incremented each time an a is found. When the loop exits, count contains the result — the total number of a's.

Think Python, 2nd edition — Allen B. Downey §8.4-8.7, pp. 75-75

37. Picture it: search stops, counter does not

Picture it

Both traverse. Only one of them can leave early.

Figure (svg): Two columns comparing the search pattern with the counter pattern on when each stops and what each returns

A search asks is it there; a counter asks how many, which cannot be answered early.

That is why a counter uses a for loop and a search usually uses an index: the counter has no reason to stop early, and the search's early return is its whole point.

38. Worked example: counting, traced

Worked example

Six characters, three matches. Watch the counter.

word = 'banana'
count = 0
for letter in word:
    if letter == 'a':
        count = count + 1
CharacterDoes it match?count after
bno matchcount 0
amatchcount 1
nno matchcount 1
a, n, atwo more matchescount 3

Initialize outside the loop.

Why: Lesson 7a's rule. Putting count = 0 inside the body would reset it every pass and the result would always be 0 or 1.

Traverse the whole string.

Why: Unlike a search, a counter has no reason to stop early: the answer depends on every character.

Increment conditionally.

Why: The update happens only when the condition holds, which is what distinguishes a counter from a length.

Figure (svg): The string banana with the three a characters highlighted as the ones that increment the counter

Three highlighted characters, three increments, and every character examined.

3. The counter examines all six characters and increments on three of them, and the answer is only complete once the loop has finished.

Verify: Check the result against a case with no matches and one with all matches.

Why: Counting 'z' in banana gives 0 and counting 'a' in 'aaa' gives 3, which equals the length. Both extremes behaving sensibly is a good check — a counter that can never reach 0 or can never reach len usually has its condition in the wrong place.

39. Predict: what does this return?

Prediction

The return is in the wrong place.

def count(word, letter):
    total = 0
    for c in word:
        if c == letter:
            total = total + 1
        return total
StageWhat happensResult
first character'b', no matchtotal stays 0
the returnruns on the first pass regardlessreturns 0
the restnever examinedthe loop ends immediately

Predict first

What does count('banana', 'a') return with this version?

  • 3
  • 1
  • 0
  • 6

Correct: 0 — the return is inside the loop and runs on the first pass, before any 'a' has been seen.

Why: The return is indented to the loop body but not to the if, so it runs on every pass — and the first pass examines 'b', which does not match, leaving total at 0. Note how the symptom depends on the first character: count('apple', 'a') would return 1 and look almost right, which is what makes this bug survive casual testing.

40. Worked example: encapsulating and generalizing it

Worked example

The book's exercise, and it is lesson 4a's two process moves.

def count(word, letter):
    total = 0
    for c in word:
        if c == letter:
            total = total + 1
    return total
MoveWhat changedBenefit
encapsulationwrap the working code in a functionit gains a name
generalizationthe word and the letter become parametersit works on anything
the returnhand the total backrather than printing it

Encapsulate the working code.

Why: Exactly lesson 4a's first process move: wrap code that already works, and give it a name.

Generalize with two parameters.

Why: The string and the letter both become parameters, so the function counts anything in anything.

Return rather than print.

Why: Lesson 6a's point: a function that prints its answer cannot be used inside a larger computation.

Figure (svg): The state of the program after each line of Worked example encapsulating and generalizing it, drawn as a ladder with one rung per traced line

The whole run at once: each drop is one line of the program.

A general counting function. Three moves from three earlier chapters — encapsulate, generalize, return — applied to six lines of working code.

Verify: Check that the counter variable was renamed and every use updated.

Why: The local is called total and the parameter is letter, and no occurrence of the original count or 'a' remains. An incomplete rename is the classic encapsulation bug from lesson 4a, and testing with a different letter — count('banana', 'n') giving 2 — catches it.

41. Trap: returning inside a counting loop

Trap

The trap

A student writes return total inside the loop, next to the increment.

Copy the shape of the search pattern

Why: Both traverse and both produce a number, so the structures look interchangeable.

The function returns after the first character, so the total is 0 or 1 — never the actual count. The two patterns differ in exactly this respect, and confusing them produces a plausible-looking wrong answer.

The fix

A count is only complete when the traversal is.

Put the return after the loop

Why: Every character must be examined before the total means anything.

Ask what evidence the answer needs

Why: A search needs one example; a count needs all of them. That question chooses between the two patterns.

The characteristic symptom is a count that is always 0 or 1, or one that equals 1 whenever the target appears at all. Either is a return that has escaped into the loop.

42. Discriminate: search or counter?

Discrimination

Ask whether one example settles the question.

Sort into buckets

For each task, which pattern applies?

a search: stop at the first match
where does the first vowel appear?; does this word contain a digit?; what is the position of the first space?
a counter: examine everything
how many vowels are there?; how many characters are uppercase?; how many words does this sentence have?
search
Each is settled by a single example: the first vowel, the first digit, the first space. Once found, there is nothing left to learn, so the traversal can stop.
count
Each depends on every element, so no early exit is possible. The answer is not known until the whole sequence has been examined.

43. Compare: the two patterns

Comparison

Fill the blanks. The differences all follow from one question.

Comparison matrix

QuestionSearchCounter
Can it stop early?yes, at the first matchno — every element affects the answer
Where does it return?inside the loop when found, after it when notafter the loop, always
What does it return?a position, or -1a total
Which loop form?usually while with an index, since the position is the answerusually for, since only the characters matter

The bottom row follows from the third: a search returns a position, so it needs one; a counter does not, so it can use the simpler loop.

44. Where these two patterns appear

Real world

They are not about strings.

Discussion prompt

Describe a search and a count you have performed on something physical — a shelf, a document, a list of names. What told you when to stop in each case?

Hint: One of them lets you stop early.

Answer:

Looking for a particular book on a shelf is a search: you stop the moment you find it, and you only conclude it is absent after checking every shelf.

Counting how many books are by one author is a counter: you have to look at all of them, and you cannot know the answer partway.

The transferable insight is the asymmetry: existence takes one example and absence takes an exhaustive check. That is why a failed search is more work than a successful one, in code and on a shelf alike.

45. Putting it together: choosing and combining the patterns

Section

Section 5

46. Slices, immutability, search and count, in one program

Concept

Real string processing combines these. A typical function traverses, tests, and builds a new string from slices — because it cannot modify the original.

def remove_letter(word, letter):
    result = ''
    for c in word:
        if c != letter:
            result = result + c
    return result
LineIts roleWhich idea it uses
result = ''an accumulator, initialized emptythe counter pattern's shape
for c in wordtraverse everythingno early exit
if c != letterthe conditiondecides what to keep
result = result + cbuild a NEW stringbecause strings are immutable

The shape is the counter pattern with a string accumulator instead of a number. Nothing is modified — every pass creates a new string and points result at it, which is what immutability requires.

Think Python, 2nd edition — Allen B. Downey §8.4-8.7, pp. 74-75

47. Picture it: an accumulator that is a string

Picture it

Same shape as a counter, with concatenation instead of addition.

Figure (svg): Two columns comparing a numeric counter with a string accumulator, showing that the shape is identical

Initialize, traverse, accumulate conditionally. Only the type of the accumulator differs.

Recognising that these are the same pattern is worth more than either one alone: an accumulator can hold a number, a string, or in chapter 10 a list, and the shape never changes.

48. Worked example: building a reversed string

Worked example

An accumulator, filled in the opposite order.

def reverse(word):
    result = ''
    for c in word:
        result = c + result
    return result
PassWhat is computedresult after
c = 'b''b' + '''b'
c = 'a''a' + 'b''ab'
c = 'n''n' + 'ab''nab'
...each new character goes in FRONTeventually 'ananab'

Notice the order of the concatenation.

Why: c + result puts the new character in front rather than behind, which is the only difference from a copying loop.

Trace two passes to see the effect.

Why: After 'b' and then 'a', result is 'ab' — the characters are accumulating in reverse order relative to the traversal.

Notice what is not needed.

Why: No index, no length, and no slicing. The reversal comes entirely from which side the concatenation happens on.

Figure (svg): A ladder showing the accumulator growing from empty to the reversed string, one character at a time

Each pass builds a new string. The old ones are discarded, which immutability requires.

'ananab'. Putting each character in front of the accumulator rather than behind it reverses the string, with no index arithmetic at all.

Verify: Check it against a slice-based reversal.

Why: Python can also reverse with word[::-1], a slice with a step, which the book does not cover here. Both give 'ananab', and agreeing with an independent method is a stronger check than tracing — which is lesson 3a's verification advice applied to a string.

49. Predict: what does this build?

Prediction

An accumulator with a conditional.

result = ''
for c in 'banana':
    if c != 'a':
        result = result + c
print(result)
CharacterKept or skippedresult
bnot 'a', keep it'b'
askipped'b'
nkept'bn'
a, n, aone kept, two skipped'bnn'

Predict first

What does this print?

  • 'banana'
  • 'bnn'
  • 'aaa'
  • ''

Correct: 'bnn' — every character except the a's, in their original order.

Why: The condition keeps characters that are not 'a', so the three a's are skipped and the b and two n's survive. Note that the original string is untouched throughout: each pass creates a new string and points result at it, because strings are immutable and there is no other way to do it.

50. Worked example: combining a search with a slice

Worked example

Find a position, then use it to cut.

def before_first(word, letter):
    i = find(word, letter)
    if i == -1:
        return word
    return word[:i]
LineWhat it doesWhich idea
findreturns a position or -1the search pattern
i == -1not found: return the whole stringthe guardian
word[:i]everything before that positionthe slice

Use the search to get a position.

Why: This is why find returns an index rather than True or False: the position is what the next step needs.

Handle the not-found case first.

Why: A guardian, from lesson 6c. Without it, word[:-1] would silently drop the last character — a wrong answer rather than an error.

Slice up to the position.

Why: word[:i] takes everything before index i, which under the boundary reading is everything before that character.

Figure (svg): The state of the program after each line of Worked example combining a search with a slice, drawn as a ladder with one rung per traced line

The whole run at once: each drop is one line of the program.

The part of the word before the first occurrence, or the whole word if the letter is absent. Three ideas from this lesson, composed.

Verify: Test the not-found case specifically.

Why: before_first('banana', 'z') returns 'banana'. Without the guardian it would return word[:-1], which is 'banan' — a plausible-looking wrong answer. That -1 is a valid index in Python is exactly why the sentinel needs checking rather than being used directly.

51. Trap: using a search result without checking it

Trap

The trap

A program calls find and uses the result as a slice index without checking for -1.

Assume the target is present

Why: In testing it always was, and the code is shorter without the check.

When it is absent, find returns -1 — which is a perfectly valid negative index. The slice silently means up to the last character instead of raising an error, so the wrong answer travels onward.

The fix

Check the sentinel before using the result.

Test for -1 immediately after the search

Why: One if statement, exactly as lesson 6c's guardian pattern prescribes.

Decide what the not-found case should do

Why: Return the whole string, return an empty one, or report a problem — but decide, rather than letting -1 be interpreted as an index.

This is a particularly nasty case because the sentinel is a legal index. In a language without negative indexing it would crash; in Python it quietly means something else, which is worse.

52. Sort: which idea does each line use?

Sorting

Four ideas from this lesson, appearing in real code.

Sort into buckets

For each line, which idea from this lesson is it using?

slicing
return word[:i]; new = 'J' + greeting[1:]
working around immutability
result = result + c
a search or counter pattern
if i == -1: return word; count = count + 1; return index
slice
Both take a range of characters using the colon operator, and both rely on the half-open convention to get the boundary right.
immut
Building a new string by concatenation rather than modifying an existing one, which is the only option strings allow.
pat
One increments a tally, one returns a found position, and one handles the not-found sentinel — the counter pattern and both exits of the search pattern.

53. Reverse engineer: what condition was used?

Reverse engineer

The input and output are given. Supply the test.

Fill in the blanks

# 'banana' became 'aaa'
result = ''
for c in 'banana':
if c == 'a':
result = result + c

Why: Keeping only the a's means the condition must be equality rather than inequality. The rest of the structure is the standard accumulator: initialize empty, traverse everything, concatenate conditionally. Reading backwards from output to condition is a genuinely useful skill — it is how you work out what a piece of code you did not write is filtering for.

54. Explain it: why does building a string need an accumulator?

Explain it

The reason is immutability, and it is worth making explicit.

Discussion prompt

A classmate asks why you cannot just modify the string as you go, adding and removing characters. Explain what forces the accumulator pattern, and what the equivalent would look like if strings could be changed.

Hint: The answer is one word, and then its consequence.

Answer:

Say: strings are immutable, so there is no operation that changes one. Every apparent modification produces a new string.

So the pattern is forced: start with an empty string, and on each pass create a new string that is the old one plus something. The name result is repointed each time, which is reassignment rather than modification.

If strings were mutable you could append in place, which is exactly what lists do in chapter 10 — and that chapter's first surprise is that mutability brings problems of its own, which is why the two types exist side by side.

55. Compare: the two patterns of this lesson

Comparison

Fill the blanks. One question decides between them.

Comparison matrix

QuestionSearchCounter
What is it asking?where is it, or is it there?how many are there?
How much does it examine?as little as possible — it stops at the first matchall of it, always
How many exits?two: found and not foundone, after the loop
What settles the answer?a single exampleevery element

The bottom row is the one to remember. Existence takes one example; a total takes all of them — and that determines everything else about the structure.

56. The procedure: writing a string-processing function

Pattern

Six steps, and the first is the one that chooses the shape.

  1. Ask what the answer depends on: a single element, or all of them. That chooses between a search and a counter.
  2. For a search, traverse with an index, return the position from inside the loop, and return the sentinel after it.
  3. For a counter, initialize the accumulator before the loop and update it conditionally inside.
  4. If the result is a string rather than a number, use the same shape with an empty string as the accumulator — because you cannot modify the original.
  5. If a position from a search feeds a slice, check the sentinel first, since -1 is a legal index and will silently mean something else.
  6. Test with the target at the first position, at the last, and absent altogether.

Step 5 is the one that produces the nastiest bug in this lesson, because the failure is silent: an unchecked -1 slices from the end rather than raising an error.

Python documentation — An Informal Introduction to Python An Informal Introduction to Python

57. Check yourself 1 of 3: slices

Check

The length is the difference of the indices.

>>> s = 'banana'
>>> s[2:5]
MarkerWhereNote
startboundary 2just after 'ba'
stopboundary 5just before the last 'a'
length5 - 23 characters

Check your understanding

What is s[2:5] when s is 'banana'?

  • A. 'nana'
  • B. 'nan' (correct)
  • C. 'ana'
  • D. 'anan'

Answer: B

Why: The slice includes the character at index 2 and excludes the one at index 5, giving three characters: n, a, n. The subtraction is the fastest check — 5 minus 2 is 3, so any answer with a different number of characters is wrong before you even work out which ones.

Why A tempts people
Four characters, which would need 6 minus 2. This is the half-open convention read as inclusive.
Why C tempts people
This is s[1:4] — the right length, starting one position too early.
Why D tempts people
Four characters again, starting at index 1. Both the start and the length are off.

58. Check yourself 2 of 3: immutability

Check

The error is about the type, not the syntax.

Check your understanding

Why does s[0] = 'H' fail for a string?

  • A. Because brackets are not allowed on the left of an assignment
  • B. Because strings are immutable and cannot be changed after they are created (correct)
  • C. Because 'H' is a string rather than a character
  • D. Because index 0 is out of range

Answer: B

Why: Strings are immutable, so there is no way to change an existing one. The message says a str object does not support item assignment — it is the type that refuses, not the syntax. Lists in chapter 10 accept exactly this line, which is what makes the distinction worth learning now.

Why A tempts people
Brackets on the left are perfectly legal syntax; that is how you assign to a list element. It is the string's immutability that makes it unavailable here.
Why C tempts people
Python has no separate character type — 'H' is a one-character string, and that is exactly what a string element is.
Why D tempts people
Index 0 is valid for any non-empty string. Reading s[0] works fine; it is assigning to it that fails.

59. Check yourself 3 of 3: the search pattern

Check

The two returns are in different places for a reason.

def find(word, letter):
    index = 0
    while index < len(word):
        if word[index] == letter:
            return index
        index = index + 1
    return -1
CaseWhere the return isWhy
foundreturn from inside the loopone match is enough
not foundreturn after the loopneeds an exhaustive check
whyabsence cannot be shown from one positionthe asymmetry

Check your understanding

Why is return -1 placed after the loop rather than inside it?

  • A. For readability only; either position works
  • B. Because you can only conclude the letter is absent after checking every position (correct)
  • C. Because a while loop cannot contain two return statements
  • D. Because -1 is a negative number

Answer: B

Why: A single non-matching character proves nothing — the letter could be at any later position. Only an exhausted loop establishes absence, so the not-found return must come after the traversal. A single match, by contrast, settles the question immediately, which is why that return can be inside.

Why A tempts people
The positions are not interchangeable. Inside the loop, the function would return -1 after examining only the first character and report not found for almost every string.
Why C tempts people
A loop can contain any number of return statements. This function has two returns, and only one of them is inside the loop.
Why D tempts people
The sign of the sentinel has nothing to do with where it is returned. -1 was chosen because no valid index is negative in this scheme, not because of where the statement sits.

60. Where this shows up outside this course

Real world

Half-open intervals are everywhere once you notice them.

Discussion prompt

Find a real-world range that includes its start and excludes its end — an opening time, a booking, a page range. Why is that convention used, and what goes wrong when people assume the end is included?

Hint: Think about two consecutive bookings for the same room.

Answer:

Room bookings are the clearest: a 9-to-10 slot and a 10-to-11 slot do not overlap, because the first excludes its end. If both included their ends, both would own 10 o'clock.

That is exactly the property of adjacent slices: s[0:5] and s[5:12] join with no overlap and no gap, sharing the boundary index.

What goes wrong when people assume inclusion is double-counting — the same hour billed twice, or the same character appearing in two slices. The half-open convention exists to make adjacency work, and the arithmetic falling out cleanly is the same fact seen from the other side.

61. Confidence wager: commit before you check

Commit first

Answer, then rate your confidence. This one has a silent failure mode.

Predict first

A program calls find, which returns -1 when the letter is absent, and uses the result directly as word[:i]. What happens when the letter is absent?

  • An IndexError, because -1 is out of range
  • It returns the whole word
  • It returns the word with its last character removed
  • It returns an empty string

Correct: It returns the word with its last character removed, because word[:-1] is a perfectly valid slice meaning everything except the last character.

Why: This is the nastiest bug in the lesson. In a language without negative indexing, -1 would be out of range and the program would crash — which would at least tell you something. In Python it is a legal index counting from the end, so the slice silently means something quite different from what was intended, and a plausible-looking wrong string travels onward. The fix is a guardian: check for -1 before using the result, and decide explicitly what the not-found case should do.

62. Explain it to someone else

Explain it

The half-open slice is the thing beginners most want justified.

Discussion prompt

A classmate is annoyed that s[0:5] gives five characters rather than six. Explain the convention using the book's picture, and give them one concrete thing it makes easier.

Hint: The picture is about boundaries, not characters.

Answer:

Say: imagine the index markers pointing BETWEEN the characters rather than at them. Then s[0:5] is everything between boundary 0 and boundary 5, which is five characters — no rule to remember, just the gap between two marks.

What it makes easier: the length of a slice is the difference of its indices, with no plus one anywhere. And two adjacent slices share a boundary, so s[:5] and s[5:] join perfectly with nothing repeated or missing.

Then give them the everyday parallel: a 9-to-10 booking and a 10-to-11 booking do not both own 10 o'clock. Half-open intervals are how ranges are made to fit together, in code and out of it.

63. Exit ticket

Exit ticket

One honest answer. It decides what the next lesson opens with.

Predict first

Which of these is still least solid for you?

  • Slices, and why the end index is excluded
  • Immutability, and building a new string instead of modifying one
  • The search pattern, and why its two returns are in different places
  • The counter pattern, and where the accumulator is initialized

Correct: Whichever you picked is the right answer — this one is for you, not for a mark.

Why: Slicing settles once the boundary picture clicks, and until it does every slice is a guess — so it is worth drawing figure 8.1 by hand. Immutability is a single fact with a large consequence, and it becomes vivid in chapter 10 when lists turn out not to share it. The search pattern's asymmetry is a genuinely general idea — existence takes one example, absence takes all of them — and it will recur for the rest of your programming life. The counter is the easiest of the four and the one whose bug is quietest, so the habit of initializing outside the loop is worth over-learning.

64. Synthesis: draw the map of this lesson

Connect it up

One page, from memory.

Draw it

Draw a six-character string with the boundary indices marked between the characters, and use it to show three slices: one from the start, one to the end, and one in the middle. Then, beside it, write the search pattern and the counter pattern side by side as skeletons, and mark on each where its return statement goes. Finally, write one sentence saying what forces a string-building loop to use an accumulator rather than modifying the original.

65. What you can do now

Recap

Three pages, and most of what string processing consists of.

If you remember one thingIt is this
From slicesThe indices point between the characters. The length is the difference.
From immutabilityNothing modifies a string. Every operation produces a new one.
From the searchOne match proves presence; only an exhausted loop proves absence.
From the counterInitialize outside the loop, update inside, return after.
From the sentinel-1 is a valid index. An unchecked not-found result fails silently.

The next lesson finishes the chapter with the methods a string already provides — including a find far more capable than the one you wrote — the in operator, and what happens when you compare two strings with a less-than sign.

Think Python, 2nd edition — Allen B. Downey §8.4-8.7, pp. 73-75 — everything on these slides traces back here

Sources

  1. Think Python, 2nd edition — Allen B. Downey — Allen B. Downey, Think Python: How to Think Like a Computer Scientist, 2nd edition (Green Tea Press, 2015), §8.4-8.7, pp. 73-75
  2. Python documentation — An Informal Introduction to Python
  3. Python documentation — Built-in Types

Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108