Chapter 8: Strings

For most of the twentieth century, a computer could store text only if someone had first decided which characters were worth a number. The oldest of these agreements, the American Standard Code for Information Interchange (ASCII), fixed 128 codes — enough for the English alphabet, the ten digits and a handful of punctuation marks, and nothing more. Every language that did not fit had to invent its own scheme, and hundreds of incompatible schemes appeared. A file typed in one system and opened in another came out as a stream of nonsense, because the second machine read the same numbers off a different list. Text that could not travel was barely text at all.

In the late 1980s a different idea took shape: one list for every writing system on earth, with room to spare. The Unicode Consortium was formed in 1991, and the first version of the Unicode Standard appeared the same year. Devanagari, Tamil, Bengali, Greek, Arabic and Han characters were given codes in the same space as the Latin letters, so that a single document could carry all of them without ambiguity.

Python 3 was built on that decision. Every string you create in Python is a sequence of Unicode characters, which is why the same variable can hold a name written in English and a name written in Hindi, and treat both the same way.

Fig. 8.0

Fig. 8.0 – A Python string holding characters from different Unicode scripts.

NoteCuriosity Corner

Q1. Why can a Python string hold a letter of an Indian language as easily as an English letter? Q2. Python has no separate data type for a single character. So how does it store just one letter? Q3. If a string cannot be changed once it is created, how can a program replace every vowel in it with a star? Q4. What does an index of -1 mean, and why would anyone count characters from the right? Q5. Two string methods search for exactly the same text, yet one returns -1 while the other stops the program. Why keep both?

This chapter will answer each of these questions as you work through it.

8.1 Introduction

Almost every program you write ends up handling text. A marks sheet carries the names of students, a bus timetable carries the names of stops, a library register carries the title of a book and the date it was issued. Numbers can be added and averaged, but text has to be read, compared, searched, cut into pieces and joined back together. Python keeps a separate data type for exactly this work, and that data type is the string.

A string is simply a run of characters kept in order — the letters of a name, the digits of a postal code, the spaces and commas of an address. The order matters: 'Meera' and 'raMee' contain the same five characters, yet they are not the same string, because the positions differ. That single idea, that the items sit in a fixed order and each one has a position, is what makes a string a sequence.

Where strings sit among data types

You met sequences in Class XI while studying Python’s data types. A sequence is an ordered collection of items in which each item is identified by an integer position. Python has three built-in sequence types, and you have already seen all three by name.

NoteDefinition

A sequence is an ordered collection of items in which every item is identified by an integer position, counted from a fixed starting point.

The three sequence types differ in what they may hold and in how they are written, as Table 8.1 shows. A string holds characters and nothing else; a list and a tuple may hold values of any type, including other strings.

Table 8.1 – the three sequence types of Python, showing how each is written and what it holds

Sequence type Written using Holds Example
String single, double or triple quotes characters only 'Bus Depot'
List square brackets values of any type [12, 25, 8]
Tuple round brackets values of any type ('Jaipur', 'Kota')

Alongside these sits the dictionary, which stores each value against a name of your choosing rather than against a position. Because the item is reached through that name and not through an integer position, a dictionary is not a sequence at all; it belongs to a category called mapping. Lists are studied in the next chapter, and tuples and dictionaries in the one after that. This chapter stays with strings.

Why text needs its own tools

Picture a string as a row of boxes, one character to a box, laid out from left to right, as in Fig. 8.1. The row is fixed once it is made, and every box has a number. Nearly everything in this chapter follows from that picture: reaching one box, reaching a stretch of boxes, walking along the whole row, or building a fresh row out of an old one.

Fig. 8.1

Fig. 8.1 – The string ‘Bus Depot’ as an ordered sequence of characters.

Q. A postal code such as 302001 is made only of digits. Why would a program store it as text rather than as a number?

A. Because it is a label, not a quantity. Nothing sensible comes of adding two postal codes or averaging them, and a postal code beginning with a zero would silently lose that zero if it were stored as a number. Digits stored as text keep every character exactly as written.

In the sections that follow you will build up strings from quotes, reach individual characters by their position, join and repeat and slice them, walk through them character by character, use the ready-made methods Python supplies for common jobs, and finally write your own functions to solve problems that no single method solves.

8.2 Strings

A string is a sequence made up of one or more characters. The characters may be letters, digits, whitespace such as a blank space or a tab, or any other symbol — a comma, a hyphen, a rupee sign. Python makes no distinction between them. As far as the language is concerned, a string is a row of characters in a fixed order, and every one of those characters counts.

The characters themselves are Unicode characters. Unicode is the international standard that gives a unique code to every character of every major writing system, so that Devanagari, Tamil, Latin and Greek letters can all sit in the same document without clashing. Because Python 3 builds its strings on Unicode, a string can hold a name written in English and a name written in Hindi with equal ease, and the same operations work on both.

NoteDefinition

A string is a sequence of zero or more Unicode characters, in which each character occupies a fixed position.

Q1. Why can a Python string hold a letter of an Indian language as easily as an English letter?

A. Because a Python 3 string stores Unicode characters, and the Unicode standard assigns codes to the letters of Indian scripts in the same code space as the Latin letters. To Python, a Devanagari letter is simply one character of the string, exactly like the letter A.

8.2.1 Creating a String

A string is created by enclosing characters in quotes. Python accepts three kinds of quote marks for this — single quotes, double quotes and triple quotes — and the resulting string is the same in each case. The quotes are only a boundary; they mark where the text begins and ends, and they are not part of the string’s contents.

>>> name1 = 'Ananya'
>>> name2 = "Ananya"
>>> name3 = '''Ananya'''
>>> name1 == name2 == name3
True

All three variables hold exactly the same six characters. So why does Python offer three forms at all? Because the choice of quote decides which characters can appear inside the text without confusing the interpreter.

Choosing between the quote marks

Suppose a string must itself contain an apostrophe. If the string is bounded by single quotes, Python reads the apostrophe as the closing quote and the rest of the line becomes nonsense. Bounding the string with double quotes solves the problem at once, because the apostrophe is then just an ordinary character.

>>> line = "Reader's card issued"
>>> print(line)
Reader's card issued

Triple quotes do something the other two cannot: they allow a string to run across more than one line. Everything typed until the closing triple quote belongs to the string, including the line breaks.

>>> notice = """Library closed on Monday
Reopens on Tuesday"""
>>> print(notice)
Library closed on Monday
Reopens on Tuesday

Notice that print() shows the two lines one below the other. The line break you typed became a character inside the string, and printing it moved the output to a fresh line.

creating a string

variable_name = 'text'
variable_name = "text"
variable_name = """text that may span
more than one line"""

Parts: variable_name is the name you choose for the string; text is the run of characters to be stored; the quotes mark the boundary and are not stored. Returns / Effect: the variable now refers to a string object holding exactly the characters written between the quotes. Example:

>>> stop = 'Sindhi Camp'
>>> print(stop)
Sindhi Camp
Strings of one character and of none

Many languages keep a separate data type for a single character. Python does not. A string of length one is simply a string that happens to hold one character, and it behaves like every other string.

>>> grade = 'A'
>>> type(grade)
<class 'str'>

At the other extreme, a string may hold nothing at all. Writing two quote marks with nothing between them creates the empty string, whose length is zero. It is a perfectly valid string, and it is the natural starting point whenever a program builds text up piece by piece.

>>> blank = ''
>>> len(blank)
0

Q2. Python has no separate data type for a single character. So how does it store just one letter?

A. As a string of length one. Writing 'A' produces a string object, not a character object, and type() confirms it is of type str. Every string operation that works on a long string works on a one-character string too.

mixing the opening and closing quote What students write:

title = 'Bus Timetable"

What happens: Python never finds a matching single quote to close the string, so the statement fails with a SyntaxError before the program runs at all. The fix: the closing quote must match the opening one — 'Bus Timetable' or "Bus Timetable". Choose one style and keep it consistent through a program.

8.2.2 Accessing Characters in a String

A string is useful only if you can reach into it. Because the characters sit in a fixed order, each one can be picked out by its position number. This technique is called indexing, and the position number is called the index. The index is written in square brackets after the string.

Counting begins at zero, not at one. The first character of a string is at index 0, the second at index 1, and the last at index \(n-1\), where \(n\) is the number of characters in the string. This takes some getting used to, but it is consistent across the whole of Python.

NoteDefinition

An index is the integer position of a character within a string, counted from 0 for the first character up to \(n-1\) for the last, where \(n\) is the length of the string.

accessing a character by index

string_name[index]

Parts: string_name is the string being read; index is an integer, or any expression that evaluates to an integer. Returns / Effect: a new string of length one, holding the character at that position. The original string is not altered. Example:

>>> msg = 'Library Card'
>>> msg[0]
'L'

Take the string msg = 'Library Card' as a working example for the rest of this section. It holds twelve characters, and the blank between the two words is one of them — it sits at index 7 and is as real a character as the letters around it.

>>> msg = 'Library Card'
>>> msg[0]
'L'
>>> msg[7]
' '
>>> msg[11]
'd'
When the index is wrong

Ask for a position that does not exist and Python refuses, rather than returning something misleading. The valid indices for this string run from 0 to 11, so index 12 is out of range and the interpreter reports an error whose last line reads:

IndexError: string index out of range

The index must also be a whole number. A fractional index such as 2.0 makes no sense — there is no character halfway between two boxes — and Python raises a TypeError for it. The exact wording of that message has changed slightly between Python versions, but the error type is the same in all of them.

The index need not be written as a plain number. Any expression that evaluates to an integer will do, and Python works the expression out first.

>>> msg[2*4]
'C'
Counting from the right

Sometimes the interesting character is at the end of a string whose length you do not know — the last letter of a name, or the final digit of a code. Python allows a negative index for exactly this. The last character has index -1, the one before it -2, and the first character has index \(-n\).

Table 8.2 sets out both ways of numbering the same string side by side. Read a column downwards and you see one character with its two indices; the character C, for instance, can be reached as msg[8] or as msg[-4].

Table 8.2 – positive and negative indices of every character in the string ‘Library Card’

Positive index 0 1 2 3 4 5 6 7 8 9 10 11
Character L i b r a r y (space) C a r d
Negative index -12 -11 -10 -9 -8 -7 -6 -5 -4 -3 -2 -1
>>> msg[-1]
'd'
>>> msg[-12]
'L'

The two schemes shown in Fig. 8.2 describe the same row of characters. Neither is more correct than the other; positive indices are natural when you are counting forward from the start, and negative indices when you are reaching backward from the end.

Fig. 8.2

Fig. 8.2 – Positive and negative indices for a string.

Q4. What does an index of -1 mean, and why would anyone count characters from the right?

A. An index of -1 means the last character of the string, -2 the second-last, and so on down to \(-n\) for the first. It is useful because it reaches the end of a string without your knowing how long the string is — msg[-1] gives the final character whether the string holds five characters or fifty.

Finding the length of a string

The built-in function len() returns the number of characters in the string passed to it. Since indexing stops at \(n-1\), len() is what tells you where a string ends.

>>> msg = 'Library Card'
>>> len(msg)
12
>>> n = len(msg)
>>> msg[n-1]
'd'
>>> msg[-n]
'L'

using the length itself as an index What students write:

last = msg[len(msg)]

What happens: the program stops with IndexError: string index out of range. For a string of twelve characters len(msg) is 12, but the highest valid index is 11. The fix: write msg[len(msg)-1], or more simply msg[-1]. The last index is always one less than the length, because counting started at zero.

Q. What does msg[-0] return — the last character or the first?

A. The first. In arithmetic \(-0\) is just \(0\), and Python sees the index as 0 before it ever looks at the string. There is no “negative zero” position, which is why negative counting has to begin at -1.

Create the string code = 'RJ14 CD 2087' in the Python shell. Without running anything, write down what you expect code[4], code[-3], code[len(code)-1] and code[3+2] to give. Then check each one at the shell and account for any answer that surprised you.

8.2.3 String is Immutable

Indexing lets you read a character. It does not let you change one. A string in Python is immutable, which means its contents are fixed at the moment it is created and cannot be altered afterwards. An attempt to assign a new character to a position is rejected.

>>> word = 'card'
>>> word[0] = 'C'

The last line of the error is:

TypeError: 'str' object does not support item assignment
NoteDefinition

An object is immutable if its contents cannot be changed after it has been created. Any operation that appears to modify it in fact produces a new object, leaving the original untouched.

This sounds like a restriction, and in one sense it is. But look at what is still perfectly allowed:

>>> word = 'card'
>>> word = 'Card'
>>> word
'Card'

Nothing was modified here. The second statement built a brand-new string 'Card' and made the name word refer to that new object instead. The original 'card' was not edited; it was simply abandoned, and Python eventually clears it away. The distinction is worth holding on to: the variable can be made to point somewhere else at any time, while the string it points to can never be edited in place.

Fig. 8.3

Fig. 8.3 – Reassigning a string variable.

So how does a program ever “change” text? By building a new string out of the old one and giving the result a name. Every operation you meet in the next sections — joining, repeating, cutting out a portion, replacing a character — works this way. It reads the original, produces a fresh string, and leaves the original exactly as it was.

Immutability is a deliberate design decision, not an oversight. Because a string can never change under you, Python can safely let two variables refer to the same string object without either one corrupting the other, and it can compute certain properties of a string once and reuse them. The cost is that repeated small edits create many short-lived objects; the benefit is that text is dependable.

Q. If strings cannot be edited, is len() computing the length afresh every time it is called?

A. It does not need to. Because the contents cannot change, the length of a string object is fixed for its whole lifetime, and Python can simply report the count it already holds. This is one of the practical gains that immutability buys.

8.3 String Operations

A string is not much use if all you can do is read one character at a time. Real work with text means joining two pieces together, repeating a pattern, asking whether one piece of text occurs inside another, and pulling out a portion of a string. Python provides four such operations — concatenation, repetition, membership and slicing — and each one is written with an operator you already know from arithmetic or from logic.

Keep the previous section’s lesson in mind while reading this one. Strings are immutable, so not one of these operations edits the string it is given. Each of them reads the original and hands back a new string.

8.3.1 Concatenation

To concatenate means to join end to end, and the operation is called concatenation. Python joins two strings with the plus operator +, placing the second string immediately after the first, with nothing inserted between them.

>>> first = 'Kabir'
>>> second = 'Nath'
>>> first + second
'KabirNath'

The result runs together because no space was asked for. A blank between the two names is itself a character, and if you want it you must supply it — as a string of one space.

>>> first + ' ' + second
'Kabir Nath'
>>> first
'Kabir'

The last two lines are the important ones. After all that joining, first still holds 'Kabir'. The operation built a new string and returned it; the originals were never touched. This is what Fig. 8.4 shows.

NoteDefinition

Concatenation is the operation that joins two strings end to end, using the + operator, and returns a new string containing the characters of the first followed by the characters of the second. Neither original string is altered.

Fig. 8.4

Fig. 8.4 – String concatenation builds a new string.

Both operands of + must be strings. A string and a number cannot be joined, because + would have no way of deciding whether you meant to add or to join.

joining a number to a string What students write:

count = 25
print('Books issued: ' + count)

What happens: the program stops with a TypeError. Python will not guess whether + means addition or concatenation when the two sides are of different types. The exact wording of the message varies a little between Python versions, but the error type is always TypeError. The fix: convert the number to a string first, using str():

count = 25
print('Books issued: ' + str(count))

8.3.2 Repetition

Repetition is the second whole-string operation, and the asterisk * performs it, repeating a string a stated number of times. One operand is the string, the other is an integer saying how many copies to run together.

>>> tune = 'Ha'
>>> tune * 3
'HaHaHa'

The commonest use of this is not for words but for drawing. A single character repeated becomes a separating line across the output of a report, and one short statement replaces twenty typed hyphens.

>>> line = '-' * 20
>>> print(line)
***
NoteDefinition

Repetition is the operation that joins a string to itself a given number of times, using the * operator with one string operand and one integer operand, and returns a new string.

The order of the two operands does not matter — 3 * tune gives the same result as tune * 3. What does matter is that exactly one of them is an integer. Two strings cannot be multiplied together, and a repetition count of zero or less produces the empty string rather than an error.

>>> 'ab' * 0
''
>>> 'ab' * -2
''

That second result is worth pausing over. Asking for minus two copies of something is meaningless, and Python treats every count below one as a request for nothing at all. It is quietly consistent, but it also means a wrong count will not announce itself; you will simply get an empty line where a row of hyphens was expected.

Print a heading for a report by first printing the title Fare Chart and then, on the next line, printing a row of equal signs exactly as long as the title. Do it without counting the characters yourself — let len() supply the number.

8.3.3 Membership

Often the question is not what a string contains at a particular position, but simply whether it contains something at all. Does this address include the word Nagar? Does this line of a file include a comma? Python answers such questions with the two membership operators, in and not in.

The in operator takes two strings. It returns True if the first appears as a substring of the second — that is, as a run of consecutive characters somewhere inside it — and False otherwise.

>>> title = 'Bus Fare Chart'
>>> 'Fare' in title
True
>>> 'Chart' in title
True
>>> 'Depot' in title
False
NoteDefinition

A substring is any run of consecutive characters taken from a string, including the string itself and the empty string.

The word consecutive is doing real work in that definition. The characters must appear together and in order, so a search for 'BusChart' fails even though every one of its characters occurs somewhere in title.

>>> 'BusChart' in title
False

Two further points catch students out. First, the comparison is case-sensitive, because an uppercase letter and a lowercase letter are different Unicode characters. Second, spaces count as characters here as everywhere else.

>>> 'fare' in title
False
>>> 'Bus Fare' in title
True

The not in operator does the opposite: it returns True when the first string does not occur inside the second.

>>> 'Depot' not in title
True
>>> 'Fare' not in title
False

Q. What does '' in title return, for any string title?

A. True. The empty string is a substring of every string, including of itself — you can find “nothing” at any position you like. It follows that a membership test on text typed by a user should check that the user actually typed something, or it will report a match on an empty entry.

8.3.4 Slicing

Indexing gives you one character. Slicing gives you a stretch of them. You state where the stretch begins and where it ends, separated by a colon inside the square brackets, and Python returns that portion as a new string.

The rule for the two numbers is not symmetrical, and it must be learnt exactly. In str1[n:m], the character at index n is included and the character at index m is not. The slice therefore runs from str1[n] up to str1[m-1], and the number of characters it contains is \(m-n\).

NoteDefinition

Slicing is the operation that extracts a portion of a string by specifying an index range, written str1[n:m], which returns a new string containing the characters from index n up to and including index m-1.

slicing a string

string_name[start:end:step]

Parts: start is the index where the slice begins and is included; end is the index where it stops and is excluded; step is optional and states how many positions to move each time, its default being 1. Any of the three may be omitted. Returns / Effect: a new string holding the selected characters. The original string is unchanged. Example:

>>> stop = 'Jaipur Junction'
>>> stop[0:6]
'Jaipur'

Take stop = 'Jaipur Junction' for the whole of this subsection. Its fifteen characters occupy indices 0 to 14, with the blank at index 6.

>>> stop = 'Jaipur Junction'
>>> stop[0:6]
'Jaipur'
>>> stop[7:15]
'Junction'

The first slice stopped at index 5 and so left the blank behind; the second began at index 7 and so skipped it. Fig. 8.5 shows why it is helpful to think of the two numbers as cuts made between characters rather than as characters themselves.

Fig. 8.5

Fig. 8.5 – Slicing a string extracts characters between index boundaries.

expecting the end index to be included What students write:

stop = 'Jaipur Junction'
print(stop[0:5])

What happens: the output is Jaipu, not Jaipur. The student counted the last character they wanted and used its index as the end. The fix: the end index is the first position not taken, so write stop[0:6]. A useful check is that \(m-n\) must equal the number of characters you expect — here \(6-0=6\), and Jaipur has six letters.

Leaving out the numbers

Either number may be omitted, and Python fills in the obvious value. Leave out the start and the slice begins at index 0; leave out the end and it runs to the end of the string. Leave out both and you get the whole string copied.

>>> stop[:6]
'Jaipur'
>>> stop[7:]
'Junction'
>>> stop[:]
'Jaipur Junction'

Slicing is also forgiving in a way that indexing is not. An end index beyond the string is quietly cut back to the end of the string rather than raising an error, and a range that runs backwards simply yields the empty string.

>>> stop[7:40]
'Junction'
>>> stop[9:3]
''

That second result is not a failure. It is Python saying, correctly, that there are no characters at or after index 9 and before index 3.

Table 8.3 – indexing compared with slicing

Feature Indexing Slicing
How it is written str1[n] — one integer str1[n:m] — a range, with a colon
What comes back a string of exactly one character a new string of \(m-n\) characters, possibly empty
Index out of range raises IndexError no error; the range is trimmed to fit the string
Reversed or equal limits not applicable returns the empty string ''
Effect on the original none — the string is only read none — a new string is built

The last row of Table 8.3 is a reminder rather than a new fact. Because strings are immutable, neither operation could alter the original even if you wanted it to.

Slicing with a step

A third number may be added, again after a colon. It is the step size, and it says how many positions to move forward after each character taken. The default step is 1, which is why every character appears in an ordinary slice.

>>> stop[0:15:2]
'Jiu ucin'
>>> stop[::3]
'Jp ni'

evaluating stop[0:15:2] where stop = 'Jaipur Junction' | Step | Index used | Character taken | Result so far | |—|—|—|—| | 1 | 0 | J | J | | 2 | 2 | i | Ji | | 3 | 4 | u | Jiu | | 4 | 6 | (space) | Jiu | | 5 | 8 | u | Jiu u | | 6 | 10 | c | Jiu uc | | 7 | 12 | i | Jiu uci | | 8 | 14 | n | Jiu ucin |

Result: the slice evaluates to 'Jiu ucin'. The trace shows that the step controls which indices are visited, while the start and end still decide where the visiting begins and stops — index 16 would come next, but 15 is the limit, so the slice ends.

Negative indices and reversal

Everything said about negative indices in Section 8.2.2 applies to slicing too. A negative start or end is counted from the right-hand end of the string.

>>> stop[-8:]
'Junction'
>>> stop[-15:-9]
'Jaipur'

A negative step is different in kind: it makes the slice run from right to left. With both limits left out and a step of -1, the whole string is produced in reverse order.

>>> stop[::-1]
'noitcnuJ rupiaJ'

This is the shortest way to reverse a string in Python, and it is worth remembering. It also fits neatly with immutability: stop itself has not been reversed, and still reads 'Jaipur Junction'. What you have been handed is a new string that happens to hold the same characters in the opposite order.

Q. Why did the designers of Python exclude the end index instead of including it?

A. Two conveniences follow from it. The length of str1[n:m] is simply \(m-n\), with no adjustment to remember. And two slices that share a boundary, such as stop[:6] and stop[6:], fit together to reproduce the whole string with no character taken twice and none left out.

Take code = 'RJ14CD2087'. Write single slice expressions that give (i) the first four characters, (ii) the last four characters without using the number 6 anywhere, and (iii) the two letters CD. Predict each result on paper before checking it at the shell.

8.4 Traversing a String

The operations of the previous section treat a string as a single object: join it, repeat it, cut a piece out of it. Many problems, though, cannot be solved that way. Counting how many words a line contains, or checking whether every character in a code is a digit, or copying a string while changing certain characters — each of these needs the program to look at the characters one at a time, from the first to the last, and do something with each one.

Visiting every character of a string in order, exactly once, is called traversal. Since a string is a sequence of known length whose characters sit at consecutive positions, traversal is a natural fit for a loop, and Python offers two ways of writing it: a for loop, which walks over the characters themselves, and a while loop, which walks over the indices.

NoteDefinition

Traversal is the process of accessing each character of a string one after another, in order, so that some action can be performed on every character.

8.4.1 Traversal Using a for Loop

You met the for loop in Chapter 6 as a way of repeating a block once for each item of a collection. A string is such a collection, so a for loop can be written directly over it. On each pass, the loop variable is given the next character of the string, and the loop ends by itself once the last character has been handed over. No index appears anywhere.

word = 'Fare'
for ch in word:
    print(ch)
F
a
r
e

Here ch is an ordinary variable, named by you. On the first pass it holds 'F', on the second 'a', and so on until 'e'. Fig. 8.6 shows the movement: the loop starts at the left end of the string and steps rightwards one cell at a time.

Fig. 8.6

Fig. 8.6 – Traversal of a string using a for loop.

Each pass through the loop can do anything you like with the character. Printing them on one line, separated by a symbol, is a small change:

word = 'Fare'
for ch in word:
    print(ch, end='-')
F-a-r-e-

Look carefully at the output. There is a hyphen after the final e as well, because end='-' applies to every call of print(), including the last. This is a small thing, but it is the kind of detail that decides whether a program’s output is right or nearly right.

treating the loop variable as an index What students write:

word = 'Fare'
for i in word:
    print(word[i])

What happens: the program stops with a TypeError, because i holds a character such as 'F', and a character cannot be used as an index. The fix: with for ch in word the variable already is the character, so write print(ch). If you genuinely need positions, loop over range(len(word)) instead and then word[i] is correct.

Traversal that accumulates a result

Printing each character is only the simplest use. More often the loop keeps a running total, or builds up an answer, and the result is used after the loop has finished. Consider a practical question: how many words are there in a line of text?

There is no need to know anything about words as such. If the words are separated by single blanks, then a line with two blanks has three words, and a line with five blanks has six. Counting blanks is enough.

counting the words in a line of text Input: a line of text in which consecutive words are separated by exactly one blank, with no blank at the beginning or the end. Output: the number of words in the line. Method:

1. Read the line of text.
2. Set the blank count to 0.
3. For each character of the line, repeat step 4.
4. If the character is a blank, increase the blank count by 1.
5. Report the blank count plus 1 as the number of words.

Why it works: every pair of neighbouring words is separated by exactly one blank, so a line of \(w\) words contains exactly \(w-1\) blanks. Adding 1 to the count of blanks recovers \(w\). Watch out: the assumption is doing real work here. Two blanks typed between words, or a stray blank at the end of the line, would each be counted as a further separator and the answer would be too high. An empty line would be reported as having one word.

counting the words in a line of text Problem: read a line of text from the user in which words are separated by single blanks, and print the number of words it contains.

line = input("Enter a line of text: ")
blanks = 0
for ch in line:
    if ch == ' ':
        blanks += 1
print("Number of words:", blanks + 1)

Sample output:

Enter a line of text: Kota Junction Road
Number of words: 3

Identifier description: | Identifier | Type | Purpose | |—|—|—| | line | string | the text entered by the user | | ch | string | the loop variable, holding one character on each pass | | blanks | integer | running count of the blank characters seen so far |

Notes: the counter blanks is set to 0 before the loop, because a counter must start from a known value and must survive from one pass to the next. Placing it inside the loop would reset it on every character and the final answer would never exceed 1. Note also that the program never uses an index — it does not need to know where the blanks are, only how many there are.

Table 8.4 – how the blank count changes while the line ‘Kota Junction Road’ is traversed

Character reached ch Is it a blank? blanks after the pass
4th a no 0
5th (space) yes 1
6th J no 1
13th (space) yes 2
18th d no 2

Table 8.4 shows only the passes where something interesting happens. The counter changes at exactly two points, and it is the value 2 that survives to the last line of the program, where 1 is added to it.

8.4.2 Traversal Using a while Loop

The for loop hides the index, which is convenient when you only care about the characters. Sometimes you care about the positions as well — you may want to report where something was found, or to compare a character with the one beside it. A while loop traverses by counting indices explicitly, and so keeps the position in plain view.

Three parts have to be got right, and all three are your responsibility: the index must be given a starting value before the loop, the condition must stop the loop before the index runs past the end of the string, and the index must be increased inside the loop.

word = 'Bus'
i = 0
while i < len(word):
    print(i, word[i])
    i += 1
0 B
1 u
2 s

The condition is i < len(word), not i <= len(word). The highest valid index is one less than the length, so the loop must end the moment i reaches the length. Fig. 8.7 shows the index as a pointer moving along the string and stepping off the right-hand end when the condition fails.

Fig. 8.7

Fig. 8.7 – Traversal of a string using a while loop over indices.

forgetting to advance the index What students write:

word = 'Bus'
i = 0
while i < len(word):
    print(word[i])

What happens: i stays at 0 for ever, so the condition never becomes false and the letter B is printed endlessly. The program has to be stopped by hand. The fix: add i += 1 as the last statement inside the loop. Every while loop needs some statement in its body that moves it towards the stopping condition.

Stopping early

There is one thing a while loop can do that a plain for loop over a string cannot: it can stop before reaching the end. Since the condition is tested afresh on every pass, you may add a second requirement to it, and the loop will end as soon as either requirement fails.

Suppose you need the position of the first blank in a route name, so that the town can be separated from the road. Once the blank has been found, there is no reason to look at the rest of the string.

route = 'Ajmer Road'
i = 0
while i < len(route) and route[i] != ' ':
    i += 1
print("First blank at index:", i)
First blank at index: 5

tracing the search for the first blank in route = 'Ajmer Road' | Test number | i | route[i] | i < len(route) and route[i] != ' ' | What happens | |—|—|—|—|—| | 1 | 0 | A | True | i becomes 1 | | 2 | 1 | j | True | i becomes 2 | | 3 | 2 | m | True | i becomes 3 | | 4 | 3 | e | True | i becomes 4 | | 5 | 4 | r | True | i becomes 5 | | 6 | 5 | (space) | False | loop ends, i stays 5 |

Result: the program prints First blank at index: 5. The trace shows that the loop stopped after examining six characters out of ten, and that i holds the answer precisely because it was not increased on the final test.

The order of the two conditions matters. i < len(route) is written first so that, in a route name containing no blank at all, the index check fails before route[i] is ever evaluated on an out-of-range index. Python stops testing an and as soon as one side is false, and that is what protects the program here.

Table 8.5 – traversal with a for loop compared with traversal with a while loop

Feature for loop over the string while loop over indices
What the loop variable holds the character itself the position of the character
Reaching a character directly, as the loop variable as str1[i], using the index
Starting, testing and advancing handled automatically by Python written by you, in three separate places
Ending early not possible without a break possible, by adding a condition
Risk of an endless loop present, if the index is never advanced present, if the index is never advanced
Best suited to acting on every character working with positions, or stopping partway

Neither loop is better in general. Choose the for loop when the task is “do this to every character”, which covers most string work, and the while loop when the position matters or when the traversal may end early.

There is a third form you will meet in other people’s programs: for i in range(len(word)), which gives a for loop that counts indices just as a while loop does. It is a useful middle path when you need positions but have no reason to stop early.

Q. A program has to check whether any character of a string is repeated immediately, as in the ee of Green. Which loop suits it better?

A. A loop over indices, because the test compares str1[i] with str1[i+1] and a plain for ch in str1 never reveals a position. The loop must also stop at len(str1)-1, since the last character has no neighbour to its right.

Write a traversal that prints only the digits of the string 'RJ14CD2087', all on one line. Then change your loop so that it also prints, after the digits, how many of them there were. Test it on a string that contains no digit at all and check that your program still behaves sensibly.

8.5 String Methods and Built-in Functions

Counting the words in a line took a loop of four statements in the last section, and that was a simple job. Converting a name to capitals, finding where a word occurs, trimming stray spaces off an entry — writing a loop for each of these, every time, would be wasteful. These are jobs every programmer needs, so Python supplies them ready-made.

They come in two shapes, and the difference in how they are written is worth getting straight at the start. A built-in function such as len() is written on its own, with the string handed to it inside the brackets. A method is written after the string, joined to it by a dot, because the method belongs to the string type itself.

NoteDefinition

A method is a function that belongs to a particular data type and is called on a value of that type, using the form value.method_name(arguments).

calling a string method

string_name.method_name(arguments)

Parts: string_name is the string the method acts on; method_name is the method being called; arguments, if any, go inside the brackets. The brackets are written even when there is no argument. Returns / Effect: a new value — usually a new string, sometimes a number or True/False. The original string is never changed. Example:

>>> 'kota'.upper()
'KOTA'

That last sentence of the snapshot is the single most important fact in this whole section, and it follows from immutability. Not one string method modifies the string it is called on. Each of them computes an answer and hands it back, and if you do not store or print that answer, it is lost.

8.5.1 Length and Case Conversion

The built-in function len() returns the number of characters in the string given to it, counting spaces and punctuation. You have already used it to control a while loop.

>>> label = 'annual sports MEET'
>>> len(label)
18

Three methods change the case of the letters. upper() returns a copy with every lowercase letter turned to uppercase, lower() does the reverse, and title() returns a copy in which the first letter of every word is uppercase and the rest lowercase.

>>> label.upper()
'ANNUAL SPORTS MEET'
>>> label.lower()
'annual sports meet'
>>> label.title()
'Annual Sports Meet'
>>> label
'annual sports MEET'

The final line is the proof of what was said above. Three methods were called, three new strings were produced and displayed, and label still holds exactly what it held at the start.

Table 8.6 – the length function and the case-conversion methods

Function or method How it is written What it returns
len() len(str1) the number of characters in str1, as an integer
upper() str1.upper() a copy of str1 with every lowercase letter converted to uppercase
lower() str1.lower() a copy of str1 with every uppercase letter converted to lowercase
title() str1.title() a copy of str1 with the first letter of each word in uppercase and the rest in lowercase

Case conversion has a practical use beyond tidy display. Comparison in Python is case-sensitive, so a search for a town name typed by a user will fail on 'kota' when the stored record says 'Kota'. Converting both to the same case before comparing removes the problem entirely.

expecting a case method to change the variable What students write:

name = 'meena'
name.upper()
print(name)

What happens: the output is meena. The method did produce 'MEENA', but nothing was done with it and it was discarded. The fix: store the result, as in name = name.upper(), or print it directly with print(name.upper()).

title() decides where a word begins by looking for any character that is not a letter. That is usually what you want, but it means an apostrophe is treated as a word boundary too, so "reader's card".title() returns "Reader'S Card". A method is a tool, not a judge of English.

8.5.2 Searching and Counting

The in operator of Section 8.3.3 answers whether a substring occurs. It does not say where, or how often. Four methods fill that gap, and a fifth pair answers questions about the two ends of a string.

Take one string for all the examples that follow.

>>> room = 'science lab and science room'

The method count() returns how many times a given substring occurs, counting non-overlapping occurrences from left to right.

>>> room.count('science')
2
>>> room.count('c')
4

The method find() returns the index at which the substring first occurs. If the substring is not present at all, it returns -1, which is a value no real index can ever take.

>>> room.find('science')
0
>>> room.find('room')
24
>>> room.find('library')
-1

Both methods accept two further arguments, a start and an end, which limit the search to a portion of the string exactly as a slice would. Give only a start and the search runs from there to the end of the string.

>>> room.count('science', 5)
1
>>> room.find('science', 5)
16

The second result is the more instructive one. find() still reports the index within the whole string, not within the searched portion, so the answer is 16 and not 11.

Two methods for one job

The method index() searches exactly as find() does and returns exactly the same index when the substring is present. The two differ only when it is absent.

>>> room.index('science')
0
>>> room.index('library')

That last call does not return a value at all. It raises an error whose final line reads:

ValueError: substring not found

This looks like a needless duplication, but the two behaviours suit two different situations, as Table 8.7 sets out.

Table 8.7 – find() compared with index()

Feature find() index()
Substring present returns the index of the first occurrence returns the index of the first occurrence
Substring absent returns -1 raises ValueError
Effect on the program continues normally; you must test the result stops, unless the error is handled
Suits the situation where absence is expected and must be handled quietly absence means the data is wrong and work should not continue

Think of a program reading a line from a register. If the line may or may not contain a comma, find() lets you check the returned value and take either path. If every line is supposed to contain a comma, index() refusing to carry on is a service: it stops the program at the bad line rather than letting a wrong answer travel silently through the rest of the calculation.

Q5. Two string methods search for exactly the same text, yet one returns -1 while the other stops the program. Why keep both?

A. The two methods are find() and index(). When the substring is present they behave identically. When it is absent, find() returns -1 and lets the program continue, which is what you want when the substring genuinely might not be there. index() raises a ValueError and halts, which is what you want when its absence means the data itself is faulty and continuing would only spread the fault.

Testing the two ends

Two further methods ask about the beginning and the end of a string. startswith() returns True if the string begins with the given substring, and endswith() returns True if it ends with it. Both return False otherwise, and both are case-sensitive.

>>> room.startswith('sci')
True
>>> room.endswith('room')
True
>>> room.endswith('Room')
False

These matter more than they look. Deciding whether a file name ends in a particular extension, or whether a vehicle code begins with a state prefix, is a startswith() or endswith() test written in one line.

Table 8.8 – the searching and counting methods

Method How it is written What it returns
count() str1.count(sub, start, end) the number of non-overlapping occurrences of sub; start and end are optional
find() str1.find(sub, start, end) the index of the first occurrence of sub, or -1 if it does not occur
index() str1.index(sub, start, end) the index of the first occurrence of sub; raises ValueError if it does not occur
startswith() str1.startswith(sub) True if str1 begins with sub, otherwise False
endswith() str1.endswith(sub) True if str1 ends with sub, otherwise False

8.5.3 Testing the Contents of a String

A program that reads text typed by a person cannot assume the person typed what was asked for. A field meant for a vehicle code may come back with a space in it; a field meant to be filled may come back empty. Python provides a family of methods whose names all begin with is, each of which examines the whole string and returns True or False.

isalnum() returns True when every character is either a letter or a digit. A space or a symbol anywhere in the string makes it False.

>>> 'RJ14CD2087'.isalnum()
True
>>> 'RJ 14'.isalnum()
False

islower() returns True when the string contains at least one letter and every letter in it is lowercase. Digits and symbols are ignored by the test, but a string with no letters at all cannot pass it. isupper() works the same way for uppercase letters.

>>> 'fare chart'.islower()
True
>>> 'fare 2026'.islower()
True
>>> '2026'.islower()
False
>>> 'FARE CHART'.isupper()
True

The second and third results together explain the rule. 'fare 2026' passes because it has lowercase letters and nothing that contradicts them; '2026' fails because there is no letter for the test to be true of.

isspace() returns True when the string is not empty and every character in it is whitespace — a blank, a tab, a newline or a carriage return. istitle() returns True when the string is in title case, that is, when the first letter of every word is uppercase and the rest lowercase.

>>> '   '.isspace()
True
>>> 'Fare Chart'.istitle()
True
>>> 'Fare chart'.istitle()
False

One rule runs through the whole family: every one of these methods returns False for the empty string. There is no character in it to satisfy any of the conditions. That makes them safe to use on text a user has typed, because an entry left blank is rejected rather than quietly passing.

Table 8.9 – the content-testing methods, all of which return False for the empty string

Method Returns True when
isalnum() every character is a letter or a digit, with no space or symbol
islower() there is at least one letter and every letter is lowercase
isupper() there is at least one letter and every letter is uppercase
isspace() every character is whitespace — blank, tab, newline or carriage return
istitle() the first letter of every word is uppercase and the rest are lowercase

Q. A form asks for a seat code made only of letters and digits. Is isalnum() on its own enough to accept the entry?

A. It is enough to reject the wrong characters, but not to accept the entry. isalnum() says nothing about the length of the code or the order of letters and digits, so 'K' and '7A9QZ4X' both pass. Checking the contents and checking the shape are two separate jobs.

8.5.4 Removing Spaces and Replacing Text

Text that arrives from a person or from a file often carries spaces that nobody wanted — a blank typed before a name, or a trailing blank left after it. Such spaces are invisible on the screen and yet they defeat every comparison, because 'Kota' and 'Kota ' are different strings.

Three methods remove them. lstrip() removes the whitespace at the left of the string, rstrip() at the right, and strip() at both ends. None of them touches whitespace inside the string, as Fig. 8.8 shows.

>>> entry = '   Ajmer Road   '
>>> entry.lstrip()
'Ajmer Road   '
>>> entry.rstrip()
'   Ajmer Road'
>>> entry.strip()
'Ajmer Road'
>>> entry
'   Ajmer Road   '

Fig. 8.8

Fig. 8.8 – Removing whitespace with the strip methods.

The method replace() takes two arguments, the text to look for and the text to put in its place, and returns a copy in which every occurrence has been replaced, not just the first.

>>> line = 'Fare: 40, Fare: 60'
>>> line.replace('Fare', 'Rate')
'Rate: 40, Rate: 60'
>>> line
'Fare: 40, Fare: 60'

Replacing text with the empty string deletes it, which is the neatest way to strip out separators.

>>> '12-34-56'.replace('-', '')
'123456'

Q3. If a string cannot be changed once it is created, how can a program replace every vowel in it with a star?

A. It does not change the original at all — it builds a new string. replace() reads the original from end to end, copies each character, puts a star where a vowel stood, and hands back the result as a fresh string. The original remains exactly as it was, which is why the result must be stored or printed if it is to be of any use.

calling a method and throwing the result away What students write:

entry = '   Ajmer Road   '
entry.strip()
print(len(entry))

What happens: the output is 16, not 10. The spaces were never removed from entry, because no string in Python is ever changed by a method. The fix: assign the result back — entry = entry.strip() — and then len(entry) gives 10.

8.5.5 Joining, Partitioning and Splitting

The last group of methods moves between a single string and its pieces. Two of them break a string apart and one puts pieces together.

split() breaks a string wherever a given separator occurs and returns the pieces as a list. Called with no argument at all, it splits on whitespace and discards the empty pieces, which is exactly what is wanted when separating a sentence into words.

>>> record = 'Kota Depot, Fare 45, Seats 32'
>>> record.split(',')
['Kota Depot', ' Fare 45', ' Seats 32']
>>> 'Kota Depot Fare 45'.split()
['Kota', 'Depot', 'Fare', '45']

Look closely at the first result. Each piece after the first begins with a blank, because the separator asked for was the comma alone and the blank after it belongs to the text. Splitting rarely gives clean pieces by itself, which is why split() and strip() are so often used one after the other.

partition() also breaks a string at a separator, but only at its first occurrence, and it returns three pieces: what came before the separator, the separator itself, and everything after it. Fig. 8.9 shows the three parts.

>>> record.partition(',')
('Kota Depot', ',', ' Fare 45, Seats 32')

Fig. 8.9

Fig. 8.9 – Breaking a string into three parts with partition().

If the separator is not present at all, partition() still returns three pieces: the whole string, and two empty strings.

>>> record.partition(';')
('Kota Depot, Fare 45, Seats 32', '', '')

join() works in the opposite direction. It is called on the separator, not on the pieces, and its argument is the collection of pieces to be joined. That reversal surprises everybody the first time.

>>> parts = ['Jaipur', 'Kota', 'Ajmer']
>>> ', '.join(parts)
'Jaipur, Kota, Ajmer'

The pieces may also be the characters of a string, since a string is itself a sequence of one-character strings.

>>> '-'.join('2026')
'2-0-2-6'

Table 8.10 – the methods that split a string apart and join pieces together

Method How it is written What it returns
split() str1.split(sep) a list of the pieces of str1 separated by sep; with no argument, splits on whitespace
partition() str1.partition(sep) three parts — before the first sep, sep itself, and after it; if sep is absent, the whole string and two empty strings
join() sep.join(pieces) a single string made of the pieces with sep placed between consecutive pieces

The separator of join() sits between the pieces and not after each of them, so joining three pieces inserts the separator twice. Every item to be joined must already be a string; a number among the pieces raises a TypeError, and must be converted with str() first.

Q. Why does split() hand back a list rather than a string?

A. Because the answer is several values, not one. The pieces are of different lengths and there is no way to keep them apart inside a single string without inventing a separator — which is precisely what was just removed. A list holds them as separate items, each reachable on its own. Lists are the subject of the next chapter.

A register line is stored as entry = ' bus 27 , sikar road , 45 '. Using only the methods of this section, produce from it the three clean pieces Bus 27, Sikar Road and 45, with no leading or trailing spaces and each in title case. Write down the order in which you applied the methods, and say why that order could not be reversed.

8.6 Handling Strings

The methods of Section 8.5 cover the jobs that arise most often, but they are general tools and your problems are particular. No method extracts the initials from a name, or picks the longest word out of a sentence, or hides all but the last four characters of a code. For work of that kind you write a user-defined function — a block of statements, given a name of your own, that takes a string in and hands an answer back.

You met functions in Chapter 7. Recall the three parts that matter here. The def line names the function and lists its parameters; the parameter receives the value supplied by the caller; and the return statement sends a value back to wherever the function was called from. Nothing else is new — what is new is the material being worked on.

One property of strings shapes every function in this section. Strings are immutable, so a function can never alter the string it is given. It reads that string, builds a fresh one or computes a number, and returns the result. If the caller wants to keep the answer, the caller must store it.

Extracting the initials of a name

Start with a small, real job. A school register holds full names in whatever case they were typed, and a summary sheet needs the initials — M.K.S. in place of meena kumari sharma. The method for this is worth stating before any code is written.

forming the initials of a full name Input: a name made of one or more words separated by whitespace. Output: a string holding the first letter of each word, in uppercase, each followed by a full stop. Method:

1. Set the result to the empty string.
2. Split the name into its words.
3. For each word, repeat steps 4 and 5.
4. Take the first character of the word and convert it to uppercase.
5. Join that character and a full stop to the end of the result.
6. Return the result.

Why it works: splitting on whitespace guarantees that each piece is a word with no blanks in it, so its character at index 0 is the letter the initial is taken from. The result grows by exactly one initial per word, so a name of \(w\) words produces \(w\) initials in the order the words appeared. Watch out: a name entered as an empty line has no words, so the loop never runs and the function returns the empty string. That is a reasonable answer, but the calling program should be prepared for it.

forming the initials of a full name Problem: read a full name from the user and print its initials in uppercase, each followed by a full stop.

def initials(name):
    result = ''
    for word in name.split():
        result += word[0].upper() + '.'
    return result

full = input("Enter the full name: ")
print("Initials:", initials(full))

Sample output:

Enter the full name: meena kumari sharma
Initials: M.K.S.

Identifier description: Table 8.11 – identifiers used in the initials program | Identifier | Type | Purpose | |—|—|—| | name | string | the parameter, receiving the full name passed to the function | | result | string | the initials collected so far | | word | string | the loop variable, holding one word on each pass | | full | string | the name entered by the user in the calling program |

Notes: result starts as the empty string because there is nothing to report before the first word is seen; it is the natural zero for building text, just as 0 is for counting. The function has no print() in it at all — it returns the string and lets the caller decide whether to display it, store it or use it in a longer line of output.

Fig. 8.10 traces what happens when the call is made: the value in full travels into the parameter name, the function works, and the returned string travels back to take the place of the call.

Fig. 8.10

Fig. 8.10 – Data flow when a string is passed to a user-defined function.

Returning a value or printing it

A beginner’s instinct is to put the print() inside the function, since printing is what finally happens. It works, once. It stops working the moment the answer is needed for anything else.

Table 8.12 – a function that returns its answer compared with one that prints it

Feature Function that returns Function that prints
What the caller receives the computed value nothing — the value None
Storing the answer possible, as x = initials(full) not possible; the answer is gone once shown
Using the answer further can be joined, compared or passed on must be recomputed
Where the output format is decided in the calling program inside the function, for every caller
Testing the function easy — compare the returned value awkward — you can only read the screen

The rule that follows is simple, and it holds well beyond this chapter: a function should compute, and the program that called it should display.

a function that prints instead of returning What students write:

def initials(name):
    for word in name.split():
        print(word[0].upper() + '.', end='')

short = initials('meena kumari sharma')
print(len(short))

What happens: the initials appear on the screen, but the program then stops with a TypeError, because a function with no return hands back None, and None has no length. The fix: build the string and return it, as in the program above. Print it in the calling program if it needs to be seen.

Hiding part of a code

Here is a second job, and one that a program has a duty to get right. A seat-booking slip should show enough of a booking code for the passenger to recognise it, and not so much that a stranger reading the slip could quote it. The usual treatment is to show the last four characters and hide the rest behind stars.

Slicing does the work directly. code[-4:] gives the last four characters whatever the length of the code, and the repetition operator produces exactly as many stars as there are characters left over. A code of four characters or fewer has nothing that could safely be shown and is returned unchanged, so that the function never produces a nonsensical string of no stars and no hidden part.

masking all but the last four characters of a code Problem: read a booking code and print it with every character except the last four replaced by a star. A code of four characters or fewer is left as it is.

def mask_code(code):
    if len(code) <= 4:
        return code
    hidden = '*' * (len(code) - 4)
    return hidden + code[-4:]

entry = input("Enter the code: ")
print("Masked code:", mask_code(entry))

Sample output:

Enter the code: RJ14CD2087
Masked code: ******2087

Identifier description: Table 8.13 – identifiers used in the code-masking program | Identifier | Type | Purpose | |—|—|—| | code | string | the parameter, receiving the booking code | | hidden | string | the run of stars that replaces the concealed characters | | entry | string | the code entered by the user in the calling program |

Notes: the function has two return statements, and the first one ends the function immediately when the code is short. Handling the awkward case first, and returning at once, keeps the rest of the function free of extra conditions. Note also that the number of stars is computed from the length rather than fixed, so the masked code is always as long as the original.

Q. Why not write hidden = '*' * 6 and be done with it?

A. Because six is right only for a ten-character code. A booking code of twelve characters would then come back two characters short, and the masked string would no longer even look like a code of the right length. Anything computed from the data should be computed, not assumed.

Finding the longest word

The third job needs the program to compare each piece with the best one found so far — a pattern you will use again and again, on marks, on prices and on distances as well as on words.

finding the longest word in a line of text Input: a line of text made of words separated by whitespace. Output: the longest word in the line; where two words are of equal greatest length, the one that appeared first. Method:

1. Set the longest word so far to the empty string.
2. Split the line into its words.
3. For each word, repeat step 4.
4. If the length of the word is greater than the length of the longest word so far,
   make this word the longest word so far.
5. Return the longest word so far.

Why it works: the empty string has length 0, so the first word compared is certain to be longer and to be taken. After that, the stored word is at every moment the longest of all the words examined so far, and this remains true after the last word is examined. Watch out: the comparison is greater than, not greater than or equal to. On a tie the stored word is kept, so the earlier of two equally long words is reported. An empty line returns the empty string.

finding the longest word in a line of text Problem: read a line of text and print its longest word. If two words are equally long, print the one that occurs first.

def longest_word(line):
    longest = ''
    for word in line.split():
        if len(word) > len(longest):
            longest = word
    return longest

text = input("Enter a line of text: ")
print("Longest word:", longest_word(text))

Sample output:

Enter a line of text: the bus reaches sikar by noon
Longest word: reaches

Identifier description: Table 8.14 – identifiers used in the longest-word program | Identifier | Type | Purpose | |—|—|—| | line | string | the parameter, receiving the line of text | | longest | string | the longest word found so far | | word | string | the loop variable, holding one word on each pass | | text | string | the line entered by the user in the calling program |

Notes: only one word is ever stored, not a list of candidates, because the question asks for one answer. The function does no printing and takes no input of its own, which means it can be called on a line read from the keyboard today and on a line read from a file next year, without a single change.

tracing longest_word('the bus reaches sikar by noon') | Pass | word | len(word) | len(longest) before | Is it greater? | longest after | |—|—|—|—|—|—| | 1 | the | 3 | 0 | yes | the | | 2 | bus | 3 | 3 | no | the | | 3 | reaches | 7 | 3 | yes | reaches | | 4 | sikar | 5 | 7 | no | reaches | | 5 | by | 2 | 7 | no | reaches | | 6 | noon | 4 | 7 | no | reaches |

Result: the function returns 'reaches'. Pass 2 is the one to study: bus is exactly as long as the, and because the test demands a strictly greater length, the earlier word survives. Change > to >= and the program would report the last of the equally long words instead.

expecting a function to change the caller’s string What students write:

def clean(text):
    text = text.strip()

entry = '   Ajmer Road   '
clean(entry)
print(len(entry))

What happens: the output is 16. The assignment inside the function pointed the parameter text at a new, trimmed string, but entry in the calling program went on referring to the original. The fix: return the new string and store it — return text.strip() inside the function, and entry = clean(entry) outside it. A function that works on a string must hand its answer back, because it cannot change what it was given.

Practical link: this task exercises user-defined functions on strings, and is filed under Python programs in the practical report file. Write a function spaced_out(word, sep) that returns a string in which the characters of word are separated by the string sep, with no separator before the first character or after the last — so spaced_out('BUS', '-') returns 'BUS' with hyphens between the letters. Read the word and the separator from the user in the calling program and print the result. Test your function on a word of one character, where no separator should appear at all.

8.7 Summary

  1. A string is a sequence of zero or more Unicode characters held in a fixed order. Because Unicode covers the writing systems of the world, a Python string can hold letters of Indian scripts as readily as Latin letters.

  2. A string is created by enclosing characters in single, double or triple quotes. The three forms produce the same string; double quotes allow an apostrophe inside the text, and triple quotes allow the text to run over more than one line. The quotes mark the boundary and are not part of the contents.

  3. Python has no separate data type for a single character. A string of length one is an ordinary string, and a pair of quotes with nothing between them is the empty string, whose length is zero.

  4. Indexing accesses one character by its position, written in square brackets. The first character is at index 0 and the last at index n-1, where n is the length of the string. An index outside this range raises an IndexError, and a fractional index raises a TypeError.

  5. Negative indices count from the right-hand end: -1 is the last character and -n the first. They reach the end of a string without the length being known.

  6. The built-in function len() returns the number of characters in a string, counting spaces and punctuation. The highest valid index is always one less than the length.

  7. A string is immutable: its contents cannot be altered after it is created, and assigning to a position raises a TypeError. A variable may be made to refer to a different string at any time, but no string is ever edited in place. Every operation that appears to change text in fact builds a new string.

  8. Four operations act on whole strings. Concatenation with + joins two strings end to end; repetition with * joins a string to itself a stated number of times; the membership operators in and not in report whether one string occurs as a run of consecutive characters inside another; and slicing extracts a portion.

  9. In a slice written str1[n:m], the start index is included and the end index excluded, so the slice holds m-n characters. Either limit may be omitted, an end index beyond the string is trimmed to fit rather than causing an error, and limits that run backwards give the empty string.

  10. A third value in a slice is the step size, which states how many positions to move after each character taken. A step of -1 with both limits omitted returns the whole string in reverse order.

  11. Traversal means visiting every character of a string once, in order. A for loop walks over the characters themselves and ends automatically; a while loop walks over the indices, which must be started, tested and advanced by the programmer. The index loop is the one to choose when positions matter or when the traversal should stop partway.

  12. A built-in function such as len() is written with the string inside its brackets, while a method is written after the string and joined to it by a dot. No string method changes the string it is called on; each returns a new value, which is lost unless it is stored or printed.

  13. Methods exist for converting case, for searching and counting, for testing what a string is made of, for removing whitespace at the ends, for replacing text, and for splitting a string into pieces or joining pieces together.

  14. find() and index() search identically but part company when the substring is absent: find() returns -1 and lets the program continue, while index() raises a ValueError and halts it. Which is right depends on whether absence is expected or is a fault in the data.

  15. Every method whose name begins with is returns False for the empty string, which makes this family safe for checking entries that a user may have left blank.

  16. Where no single method solves a problem, a user-defined function is written. Such a function receives a string through its parameter, computes an answer, and returns it. It cannot alter the string it was given, so the answer must be handed back and stored by the caller; a function that prints instead of returning gives the caller nothing to work with.

8.8 Key Terms

  1. ASCII — American Standard Code for Information Interchange; an early character-coding scheme that fixed 128 codes, enough for English letters, digits and common punctuation.

  2. Concatenation — the operation that joins two strings end to end using the + operator, returning a new string and leaving both originals unchanged.

  3. Empty string — a string containing no characters at all, written as a pair of quotes with nothing between them, and having a length of zero.

  4. Immutable — describing a value whose contents cannot be altered after it has been created; every operation that seems to change a string in fact produces a new one.

  5. Indexing — accessing a single character of a string by its position, written in square brackets, with 0 for the first character and n-1 for the last.

  6. Mapping — a category of data type in which a value is reached through a name of the programmer’s choosing rather than through an integer position; the dictionary is Python’s mapping type.

  7. Membership operators — in and not in, which report whether one string occurs as a run of consecutive characters inside another, returning True or False.

  8. Method — a function that belongs to a data type and is called on a value of that type using dot notation, as in str1.upper().

  9. Repetition — the operation that joins a string to itself a stated number of times using the * operator, with a count of zero or less giving the empty string.

  10. Sequence — an ordered collection of items in which every item is identified by an integer position; strings, lists and tuples are Python’s sequence types.

  11. Slicing — extracting a portion of a string by giving an index range, str1[n:m], in which the start index is included and the end index excluded.

  12. Step size — the optional third value in a slice, stating how many positions to move after each character is taken; the default is 1, and -1 works backwards through the string.

  13. String — a sequence of zero or more Unicode characters in a fixed order, created by enclosing characters in single, double or triple quotes.

  14. Substring — any run of consecutive characters taken from a string, including the whole string itself and the empty string.

  15. Traversal — visiting each character of a string once, in order, usually with a for loop over the characters or a while loop over the indices.

  16. Unicode — the international standard that assigns a unique code to every character of every major writing system, and on which Python 3 strings are built.

  17. User-defined function — a named block of statements written by the programmer, which receives values through its parameters and sends a result back with return.

8.9 Points to Ponder

  1. Python lets you change a list after it is built, but not a string. If both are sequences, why should one of them be fixed for life? What would a program gain, and what would it lose, if strings could be edited character by character?

  2. The end index of a slice is excluded, which is the rule students most often get wrong. Suppose the language had been designed the other way, so that str1[n:m] included the character at m. Which of the conveniences you now rely on would disappear?

  3. find() returns -1 when the substring is absent, while index() stops the program. A language could have offered only one of them. Which single behaviour would you keep, and what kind of mistake would become easier to make once the other was gone?

  4. Python has no separate data type for a single character, and treats one letter as a string of length one. Is anything actually lost by that decision? Think about what a program can no longer say about the difference between one character and many.

  5. Splitting a string returns a list, not a string. Could a language have made split() return a string with the pieces separated by some special character instead? What would go wrong the first time the text itself contained that character?

  6. An index beyond the end of a string raises an error, but a slice that runs beyond the end is quietly trimmed to fit. Is that silence a convenience for the programmer, or a place where a wrong program can go on looking right?

8.10 Objective Questions

  1. What is the value of len('Bus Stand')? [1 mark]
    1. 8
    2. 9
    3. 10
    4. The expression raises an error
  2. Which one of the following expressions raises an error, given town = 'Kota'? [1 mark]
    1. town[-4]
    2. town[0:9]
    3. town[4]
    4. town[::-1]
  3. The value of the expression 'Ha' * 0 is [1 mark]
    1. 'Ha'
    2. 0
    3. ''
    4. None
  4. Which method returns -1 when the substring being looked for is not present? [1 mark]
    1. index()
    2. find()
    3. count()
    4. partition()
  5. Assertion (A): A string method never alters the string it is called on. Reason (R): Strings in Python are immutable. [1 mark]
    1. Both A and R are true, and R is the correct explanation of A.
    2. Both A and R are true, but R is not the correct explanation of A.
    3. A is true, but R is false.
    4. A is false, but R is true.
  6. Assertion (A): The expression '2026'.islower() evaluates to True. Reason (R): islower() returns True only when the string contains at least one letter and every letter in it is lowercase. [1 mark]
    1. Both A and R are true, and R is the correct explanation of A.
    2. Both A and R are true, but R is not the correct explanation of A.
    3. A is true, but R is false.
    4. A is false, but R is true.
  7. Match each method in Column I with what it returns in Column II. [1 mark]
Column I Column II
a. strip() i. three parts — before the separator, the separator, and after it
b. split() ii. a copy with the whitespace removed from both ends
c. count() iii. a list of the pieces lying between the separators
d. partition() iv. the number of non-overlapping occurrences of a substring
  1. Fill in the blank: in a string of length n, the index of the last character is ______ and the negative index of the first character is ______. [1 mark]

  2. State whether the following statement is true or false: a slice whose start index is greater than its end index raises an IndexError. [1 mark]

  3. Write the output of the following statement. [1 mark]

print('Fare Chart'[::2])
  1. Write the full form of ASCII. [1 mark]

8.11 Check Your Concepts

  1. Define a substring. Give one example of a substring of 'Sikar Road' and one run of characters taken from it that is not a substring. [2 marks]

  2. State what the built-in function len() returns, and explain why the largest valid index of a string is always one less than that value. [2 marks]

  3. Differentiate, in two points, between the in operator and the find() method. [2 marks]

  4. Write a single expression that reverses the string stored in title, and state what title itself holds after the expression is evaluated. [2 marks]

  5. Explain, with a short example, why calling entry.strip() on its own leaves entry unchanged, and give the correct way to keep the trimmed text. [3 marks]

  6. Differentiate between find() and index(), and describe one situation in which each of them is the better choice. [3 marks]

  7. Write a user-defined function count_upper(text) that returns the number of uppercase letters in the string passed to it. [3 marks]

  8. Explain the three responsibilities a programmer takes on when traversing a string with a while loop, and state what goes wrong if each one is neglected. [4 marks]

  9. Write a user-defined function last_word(line) that returns the last word of a line of text whose words are separated by blanks. State what your function returns for a line that is empty, and justify that behaviour. [4 marks]

  10. Write a user-defined function digit_sum(text) that returns the sum of the digits appearing anywhere in the string passed to it. Explain how your function decides that a character is a digit, and state what it returns for a string containing no digit at all. [5 marks]

8.12 Output & Debugging

  1. Predict the output of the following fragment. [2 marks]
s = 'Depot'
print(s[1:4], s[-2:], sep='|')
  1. Predict the output of the following fragment, and account for the number of items in the result. [2 marks]
line = 'a-b--c'
print(line.split('-'))
  1. State what the following fragment prints, and say in one sentence what it computes for any string. [3 marks]
word = 'Sikar'
count = 0
for ch in word:
    if ch in 'aeiouAEIOU':
        count += 1
print(count)
  1. The following fragment is intended to print every character of the code on a separate line, but it fails. Identify the error, name the exception raised, and rewrite the faulty line correctly. [3 marks]
code = 'RJ2087'
i = 0
while i <= len(code):
    print(code[i])
    i += 1
  1. State what the following fragment prints, and explain why the output is not what the writer of the code seems to have expected. [2 marks]
name = 'meena'
name.title()
print(name)
  1. Write the output of the following fragment, and explain why the second value differs from the first. [3 marks]
text = 'bus stand'
print(text.find('s'), text.find('s', 3), text.count('s'))

8.13 Case-Based Questions

  1. A school library keeps each issue record as a single line of text, with three fields separated by a vertical bar. One such line is stored in the variable record: [4 marks]
record = 'B1042|Bird Watching Guide|14-07-2026'
a. Write a single expression that gives the book code `B1042` from `record`, using slicing only. [1 mark]
b. Write a single expression that gives the date of issue, without assuming the length of the book title. [1 mark]
c. Write one statement that separates `record` into its three fields in a single step, and state what your statement would give if a line contained no vertical bar at all. [2 marks]
  1. A bus depot collects passenger destinations through a form. Entries reach the program exactly as they were typed, so they may carry stray spaces and inconsistent case, as in: [5 marks]
entry = '   sikar road   '
a. Write the statements that turn `entry` into `'Sikar Road'`, and explain why the result of each method call must be assigned to a variable. [2 marks]
b. Write a user-defined function `is_blank(entry)` that returns `True` when the entry is empty or consists only of whitespace, and `False` otherwise. Take care that your function handles an entry of zero length correctly. [2 marks]
c. Give one reason why a cleaned entry should be converted to a common case before it is compared with the destinations already stored. [1 mark]

8.14 Programming Practice

  1. Write a user-defined function only_digits(text) that returns a new string holding just the digits of text, in the order in which they appear. Submit the function together with a calling program that reads a line from the user, and show the output for the input RJ 14 CD 2087. [3 marks]

  2. Write a complete program that reads one line of text from the user and prints three things: the total number of characters in the line, the number of characters that are not blanks, and the line converted to title case. Submit the program and an identifier description table. [4 marks]

  3. Write a user-defined function same_ends(word) that returns True when a word begins and ends with the same letter, ignoring the difference between uppercase and lowercase, and False otherwise. Submit the function, and state clearly what your function returns for a word of a single letter and why. [4 marks]

  4. A notice board can show only short words. Write a user-defined function short_form(line) that returns a new line in which every word longer than four characters is replaced by its first four characters followed by a full stop, while shorter words are left unchanged. Submit the function, an identifier description table, and one sample input with the output it produces. [5 marks]