Strings and text · about 35 min

Strings are sequences: indexing, slicing and immutability

Why this matters

Text is everywhere: names, file paths, log lines, CSV rows, user commands. Almost every real program pulls pieces out of text: the extension of a file name, the first letter of a name, the date at the start of a log line. Python treats a string as an ordered sequence of characters, and the same indexing and slicing rules you learn here work on lists, tuples, bytes and ranges later. Learn them once, carefully, and you will use them every day.

A string is a sequence of characters

A string (type str) is an ordered, fixed sequence of characters. Each character has a position called its index, starting at 0:

 text = "Python"

  index:   0   1   2   3   4   5
         +---+---+---+---+---+---+
         | P | y | t | h | o | n |
         +---+---+---+---+---+---+
 negative: -6  -5  -4  -3  -2  -1

len() gives the number of characters:

text = "Python"
print(len(text))
print(len(""))
6
0

The empty string "" is a perfectly good string of length 0. Because indexes start at 0, the last valid index is always len(text) - 1.

Note: Why start at 0? An index is best thought of as an offset from the start: the first character is 0 steps from the start. Most languages descended from C work this way, and it makes slice arithmetic tidy, as you will see.

Indexing

Square brackets after a string pick out one character. The result is itself a string, of length 1 (Python has no separate "character" type):

text = "Python"
print(text[0])
print(text[5])
print(text[-1])
print(text[-2])
P
n
n
o

Negative indexes count from the end: -1 is the last character, -2 the one before it. Internally Python adds len(text) to a negative index, so text[-1] means text[6 - 1], which is text[5].

Asking for a position that does not exist is an error:

print(text[6])
Traceback (most recent call last):
  File "main.py", line 2, in <module>
    print(text[6])
          ~~~~^^^
IndexError: string index out of range

Slicing

A slice extracts a substring: text[start:stop] gives the characters from index start up to but not including stop.

text = "Python"
print(text[0:2])
print(text[2:6])
print(text[1:4])
Py
thon
yth

The best mental model: indexes point at the gaps between characters, and a slice takes everything between two gaps.

   gap:  0   1   2   3   4   5   6
         | P | y | t | h | o | n |
              \_________/
               text[1:4] = "yth"

This "half-open" convention has nice properties:

Both bounds are optional. A missing start means "from the beginning", a missing stop means "to the end":

filename = "report.txt"
print(filename[:6])
print(filename[-3:])
print(filename[:])
report
txt
report.txt

filename[-3:] (the last three characters) is a common idiom. filename[:] is a full copy of the string.

Unlike indexing, slicing is forgiving: bounds beyond the end are clipped, and a slice that selects nothing gives "":

s = "abc"
print(s[1:100])
print(repr(s[5:]))
bc
''

The step

A third number, the step, says how far to move each time: text[start:stop:step].

digits = "0123456789"
print(digits[::2])
print(digits[1::2])
print(digits[::3])
print(digits[::-1])
print(digits[8:2:-2])
02468
13579
0369
9876543210
864

With a negative step, the slice walks backwards, so start should be the larger index. The idiom s[::-1] reverses a string: start at the end, go to the beginning, one step back at a time.

Membership with in

The in operator asks whether one string occurs inside another; not in is its negation. The result is a boolean:

email = "ada@example.com"
print("@" in email)
print("example" in email)
print(" " not in email)
print("Ada" in email)
True
True
True
False

The last line is False because comparisons are case-sensitive: "A" and "a" are different characters.

Immutability

Strings are immutable: a string object cannot be changed after it is created. You cannot assign to an index:

s = "jello"
s[0] = "h"
Traceback (most recent call last):
  File "main.py", line 2, in <module>
    s[0] = "h"
    ~^^^
TypeError: 'str' object does not support item assignment

Instead you build a new string from pieces and rebind the name:

s = "jello"
s = "h" + s[1:]
print(s)
hello

Recall from the object-model lesson that a name is a label bound to an object. After s = "h" + s[1:], the name s points at a brand-new string object "hello"; the old "jello" object was never modified.

before:  s ---> "jello"
after:   s ---> "hello"        "jello"  (no longer referenced, freed)

Why make strings immutable? Several good reasons:

The cost is that building a long string piece by piece creates many intermediate objects. The next lesson shows join, the efficient way to assemble many pieces.

Common mistakes

Recap

Going further: in the REPL, try s = "abcdef" and predict s[-2:], s[:-2], s[::-2] and s[4:1] before pressing Enter. Then read "Common Sequence Operations" in the Python library documentation (section "Built-in Types").

Exercises

Exercise 1 · quiz

Predict the characters

Given word = "Python", what does this print? Type the output as one line with the three results separated by single spaces.

print(word[0], word[-1], word[2:4])
Answer

Indexes start at 0, so word[0] is P. Negative indexes count from the end: word[-1] is the last character, n. A slice word[2:4] starts at index 2 and stops before index 4, giving the characters at 2 and 3: t and h.

Exercise 2 · write code

First, last, middle

Read one line of text (at least 3 characters long) and print three lines:

  1. first: followed by its first character
  2. last: followed by its last character
  3. middle: followed by everything except the first and last characters

For input banana the output must be exactly:

first: b
last: a
middle: anan

File: main.py

Starting point

text = input()
print("first:", text[1])

Example

input

banana

expected output

first: b
last: a
middle: anan

Example

input

cat

expected output

first: c
last: t
middle: a
Hint 1

The first character is at index 0, the last at index -1.

Hint 2

A slice can use negative numbers too: text[1:-1] drops one character from each end.

Reference solution
text = input()
print("first:", text[0])
print("last:", text[-1])
print("middle:", text[1:-1])

Exercise 3 · quiz

Changing a string

What happens when this runs?

s = "jello"
s[0] = "h"
print(s)
Answer
  • TypeError: 'str' object does not support item assignment

Strings are immutable: once a string object exists, its characters can never change. To get hello you build a new string, for example s = "h" + s[1:], and rebind the name s to it. The old string object is untouched (and is freed once nothing refers to it).

Exercise 4 · write code

Slices with a step

Read one line of text and print three lines:

  1. the text reversed
  2. every second character, starting with the first (characters at index 0, 2, 4, ...)
  3. yes if the text reads the same forwards and backwards (a palindrome, compared exactly, case and spaces included), otherwise no

For input racecar:

racecar
rccr
yes

For input python:

nohtyp
pto
no

Use slices only, no loops. You may print the third line with a conditional expression such as print("yes" if a == b else "no"), which picks the first value when the condition is true.

File: main.py

Starting point

text = input()
print(text)

Example

input

racecar

expected output

racecar
rccr
yes

Example

input

python

expected output

nohtyp
pto
no
Hint 1

A slice has three parts: start:stop:step. Leaving start and stop out means the whole string.

Hint 2

A step of -1 walks backwards; a step of 2 takes every second character.

Hint 3

Compare text == text[::-1].

Reference solution
text = input()
print(text[::-1])
print(text[::2])
print("yes" if text == text[::-1] else "no")

Exercise 5 · quiz

Slices past the end

Given s = "abc", which of these expressions raise an error? Select all that apply.

Answer
  • s[5]

  • s[-4]

Indexing a single position that does not exist (s[5], s[-4]) raises IndexError: string index out of range. Slicing never raises for out-of-range bounds: Python clips them to the string, so s[1:10] is "bc" and s[10:] is the empty string "". This forgiving behavior is handy but can hide bugs, so check lengths when it matters.