Python Part I · Module 4 · Strings
Indexing: Reaching One Character
A string holds its characters in a definite order, and Python gives every position a number so you can name one. That sounds trivial and mostly is, but three details cause nearly all the confusion beginners have with text: the numbering starts at zero rather than one, the negative numbers reach backwards from the end in a way that is not the mirror image people expect, and what you get back from an index is not a character in the sense other languages mean — it is a perfectly ordinary one-character string. This chapter settles all three, and meets the IndexError you will eventually cause, so that when you do you already know what it means.
Foundation12 min readPythonPart IFundamentalsStringsIndexing
Part I · Module 4 · Chapter 4.4 · Indexing: Reaching One Character
1. Asking for One Character
Sometimes the question is about a single character rather than the whole string. Does this path end in a separator? Is this signal name prefixed with an underscore? Both need a way to say the character at that position.
Python's notation for it is square brackets after the string, with a position number inside:
name = "axi_smoke"
print(name[0])aname[0] is read aloud as "name at zero" or "name sub zero". It is an expression, like everything in Module 3 — it evaluates to a value, so it can be printed, named or compared.
And the value it evaluates to is the first character, even though the number is zero. That is the first thing to get straight.
2. Positions Count From Zero
Python numbers the positions in a string starting at 0. A string of five characters therefore has positions 0, 1, 2, 3 and 4 — and no position 5.
Here is the whole picture for a short string. The numbers above are the ordinary indexes; the numbers below are the negative ones from Section 5:
0 1 2 3 4
+----+----+----+----+----+
"CLOCK" | C | L | O | C | K |
+----+----+----+----+----+
-5 -4 -3 -2 -1Read a column at a time. The C at the front sits in the cell labelled 0 above and -5 below, so both t[0] and t[-5] reach it. The K at the end sits in the cell labelled 4 and -1.
Check it against Python:
t = "CLOCK"
print(t[0], t[1], t[2], t[3], t[4])C L O C KThe same numbering on a longer, more realistic string:
name = "axi_smoke"
print(name[0])
print(name[1])
print(name[3])
print(name[8])a
x
_
ePosition 3 is the underscore, and position 8 is the final e. Nine characters, positions 0 through 8.
3. What Indexing Gives You Back
This section is short but it is the conceptual heart of the chapter.
name = "axi_smoke"
first = name[0]
print(first)
print(type(first))
print(len(first))a
<class 'str'>
1Look at what type() reported: str. Not a character type — Python does not have one. Indexing a string gives you back a string, one character long.
That is not a technicality; it is what makes indexing easy to work with. Because the result is an ordinary string, everything you already know applies to it:
name = "axi_smoke"
first = name[0]
print(first + first)
print(name[0] == "a")aa
TrueYou concatenated it with + from Chapter 4.3 and compared it with == from Chapter 3.4, and neither needed anything special. A one-character string is just a string.
4. Reaching the Last Character the Long Way
You often want the last character without knowing how long the string is. One way uses len():
name = "axi_smoke"
print(len(name))
print(name[len(name) - 1])9
eNine characters, so the last position is 8, which is len(name) - 1. That expression is correct and you should be able to read it, because the - 1 is exactly the zero-based numbering showing up again: the last valid index is always one less than the length.
It is also clumsy, which is why Python offers something better.
5. Negative Indexes Reach From the End
A negative index counts backwards from the end of the string:
name = "axi_smoke"
print(name[-1])
print(name[-2])
print(name[-9])e
k
aname[-1] is the last character. name[-2] is the one before it. name[-9], on a nine-character string, is the first.
The two numbering systems address the same characters, so for any string the first character can be reached both ways:
name = "axi_smoke"
print(name[0] == name[-9])TrueBoth reached the same a. The ruler in Section 2 shows why: the two rows of numbers label the same cells.
Negative indexing is genuinely useful. This tells you whether a path ends in a separator without measuring anything:
p = "runs/axi_smoke/"
print(p[-1])
print(p[-1] == "/")/
TrueCompare that with p[len(p) - 1]. Same answer, more noise.
6. Escapes Occupy One Position
A detail that follows from Chapter 4.2 and catches people out. An escape sequence is one character, so it occupies exactly one position:
e = "a\nb"
print(len(e))
print(e[1] == "\n")3
TrueThree positions — 0, 1, 2 — and position 1 is the newline. It is not two positions, and e[1] is not a backslash. If you predicted that e[1] would be \ and e[2] would be n, re-read Chapter 4.2 Section 3 before going on to slicing, because every slice boundary in the next chapter depends on getting this right.
7. Broken on Purpose
Five failures, all executed. The messages are exactly what Python prints.
Asking for a position that does not exist. A nine-character string has no position 9:
name = "axi_smoke"
print(name[9])IndexError: string index out of rangeThis is the error the canonical curriculum wants you to be able to read, so read it literally: the index was out of the range of valid positions. For a string of length 9 the valid range is 0 to 8, and 9 is one past the end. Whenever you see IndexError, the first thing to check is an off-by-one against the length.
Going too far negative fails the same way, because -10 is one before the start of a nine-character string:
name = "axi_smoke"
print(name[-10])IndexError: string index out of rangeAn empty string has no valid index at all — not even 0, because there is no first character to reach:
print(""[0])IndexError: string index out of rangeThat one matters in practice. Text that arrives from a file or a tool can legitimately be empty, and line[0] on an empty line raises rather than returning something blank.
The index must be a whole number. A float is refused even when it looks whole:
print("abc"[1.0])TypeError: string indices must be integers, not 'float'Note this is a TypeError, not an IndexError — the position was not out of range, it was the wrong kind of value entirely. Chapter 2.6 explains why 1.0 and 1 are different types. This shows up when a position is calculated with /, which as Chapter 3.2 established always produces a float; // keeps it an integer.
Assigning to a position is refused. This is the promised evidence for Section 3:
text = "PASS"
text[0] = "F"TypeError: 'str' object does not support item assignmentReading a position works; writing one does not exist as an operation. "Does not support item assignment" is Python saying: this kind of value has no way to have one of its items replaced. Chapter 4.11 shows what to do instead.
8. Predict, Run, Observe
Given this string, predict all six before reading on:
sig = "clk_en"
print(sig[0])
print(sig[3])
print(sig[-1])
print(sig[-6])
print(len(sig))
print(sig[0] == sig[-6])The verified output:
c
_
n
c
6
TruePosition 3 is the underscore — count c, l, k, then _ at index 3. sig[-1] is the final n. sig[-6] is the first character of a six-character string, so it equals sig[0], which is why the last line is True.
Now one more, and this is the one to be careful about:
sig = "clk_en"
print(sig[6])That is an IndexError. The string has six characters, so the last valid index is 5. If you predicted n you were thinking one-based; sig[6] is one past the end for exactly the same reason name[9] was in Section 7.
9. Exercises
Exercise 1 — read the ruler. For word = "RESET", give the character at each of these without running anything, then check: [0], [2], [4], [-1], [-5].
Exercise 2 — both ways. Write two different expressions that each produce the last character of path = "sim.log". Say in one sentence which you would prefer in real code and why.
Exercise 3 — what type? Predict the output of all three lines, then run them:
c = "PASS"[0]
print(c)
print(type(c))
print(len(c))Exercise 4 — find the off-by-one. This is meant to print the last character of the name but raises IndexError. Say what the valid index range is for this string and fix the line:
name = "axi_smoke"
print(name[len(name)])Exercise 5 — the empty line. Explain in one sentence why line[0] is risky for text that came from a file, and name the error it raises when the text is empty.
Exercise 6 — prove it is not a cell. Write the two lines that attempt to change the first character of "FAIL" to "P", run them, and write down the exact error. Then say in one sentence what that error tells you about how Python stores text.
Exercise 7 — count the escape. For line = "PASS\n", predict len(line), line[-1], and whether line[-1] == "\n" is True. Explain your answer for the length in terms of Chapter 4.2.
10. Summary
name[i]reaches the character at positioni, and it is an ordinary expression that evaluates to a value.- Positions start at 0.
name[0]is the first character;name[1]is the second. - The last valid index is
len(name) - 1. That- 1is the zero-based numbering reappearing. - Indexing gives back a one-character string, of type
str. Python has no separate character type, so everything you know about strings applies to the result. - A Python string is not an array of writable character cells. Reading a position works; assigning to one is a
TypeError. -1is the last character,-2the one before it,-len(name)the first. There is no-0.- An escape sequence occupies one position. In
"a\nb", position 1 is the newline. IndexError: string index out of rangemeans the position does not exist — check it against the length. An empty string has no valid index, not even 0.TypeError: string indices must be integersis a different failure — the index was the wrong type, typically a float from/.
Reaching one character is useful; reaching a run of them is more useful. The next chapter takes pieces out of a string.
Where this fits
Part of the Python curriculum.
