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:
text[a:b]has exactlyb - acharacters.text[:k] + text[k:]is always the original string, for anyk.
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:
- Safety: if two names refer to the same string, neither can surprise the other by changing it.
- Hashing: immutable values can be used as dictionary keys (you will see why in the collections and internals modules).
- Efficiency: CPython can share identical string objects, and slicing can never corrupt the original.
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
- Off-by-one in slices.
text[0:3]has three characters (0, 1, 2), not four. If you want characters up to and including index 3, writetext[0:4]. - Using
len(text)as an index.text[len(text)]is always anIndexError; the last character istext[len(text) - 1], or simplytext[-1]. - Indexing an empty string.
""[0]raisesIndexError. When input might be empty, checkif text:first (an empty string is falsy). - Expecting slices to complain.
s[10:20]silently returns"". If an empty result is suspicious, it may be your bounds. - Trying to modify in place. Any "change" to a string is really "make a new string and rebind the name".
Recap
- A string is an immutable, ordered sequence of characters;
len(s)counts them. s[i]gets one character; indexes run from 0 tolen(s) - 1, and negative indexes count from the end.s[start:stop:step]gets a substring;stopis excluded, bounds are optional and clipped, ands[::-1]reverses.inandnot intest for substrings, case-sensitively.- Assigning to
s[i]is aTypeError; build a new string instead.
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").