A Sequence of Characters
A string is an immutable indexed sequence of characters you can slice, search, and rebuild into new strings. · 11 min
You have printed strings and read them from input since the first program. Now look inside one. A string is text stored as a sequence of characters in order — much like a list, but of letters, digits, and spaces. Because it is a sequence, you can reach any character by its index and pull out a run of characters called a slice. One difference sets strings apart from lists: a string cannot be changed once made. To transform text, you build a new string from the old one. Reach in, cut out a piece, search it, rebuild it — that is the work of this folio.
Guess before you learn
A string is indexed like a list:
``
s = 'PYTHON'
`
What is s[0]`?
It is 'P' — the character at index 0, and itself a one-character string. Counting starts at 0, exactly as it did for lists, so s[1] is 'Y'. If you expected the whole word, keep that pencil mark: indexing pulls out a single character, and the next section turns single characters into slices.
9–12
3–5
A string is text: a row of characters kept in order, written in quotes like 'cat'. Each character has a position number, an index, starting at 0 — so 'cat'[0] is 'c'. You can read any character and copy out a piece of the text. What you cannot do is change a character in place. To get different text, you make a new string.
6–8
A string is a sequence of characters stored in order, written in quotes: s = 'PYTHON'. Each character sits at an index counting from 0, so s[0] is 'P' and s[-1] is 'N', and len(s) is 6. A slice copies a run of characters: s[0:3] is 'PYT', taking indices 0, 1, and 2 but stopping before 3. Strings are immutable — once made, their characters cannot be reassigned. So s.upper() and s + '!' do not change s; they each return a new string, which you must store if you want to keep it.
9–12
A string is an immutable, index-addressed sequence of characters. Indexing is zero-based, with negatives counting from the end: for s = 'PYTHON', s[0] is 'P' and s[-1] is 'N'. A slice s[a:b] returns the characters from index a up to but not including b, so s[2:5] is 'THO'; omit an end to run to the edge (s[:3], s[3:]). Immutability means no character can be reassigned — s[0] = 'J' raises a TypeError. Every transforming operation instead returns a new string: s.upper(), s.replace('O', '0'), and s + '!' all leave s untouched and hand back fresh text you can name and reuse.
K–2
A word is beads in a row, one letter each. You can point at any bead to read its letter. But the beads are glued in place. To change the word, you thread a whole new string.
Undergrad
A Python str is an immutable sequence of Unicode code points. Immutability underwrites hashability — strings can be dict keys and set members — and lets the runtime share and cache them freely, since no reference can observe a mutation. Indexing and slicing follow the sequence protocol (zero-based, half-open slices, negative wrap-around); slicing allocates a new string. Because in-place edits are impossible, string 'transformation' is always construction: methods like upper, replace, and strip are pure functions returning new objects. This is why repeated concatenation in a loop is quietly O(n squared) unless you accumulate pieces in a list and join once.
Postgrad
Model str as an immutable finite sequence over the Unicode code-point alphabet, carrying the full sequence protocol minus mutation. Immutability is the load-bearing invariant: it makes value and identity coincide for equality purposes, guarantees hash stability, and enables safe interning and structural sharing. All operators are total pure functions returning fresh strings, so an algorithm's cost model must count the allocations that in-place list algorithms avoid — hence the ''.join(parts) idiom over iterated +. Slicing is a half-open sub-sequence operation, s[a:b] with the fencepost stop exclusive; the same off-by-one discipline that governed loop bounds governs slice bounds, and for the same reason.
slice
A run of characters copied out of a string by a range of indices. s[a:b] takes index a up to but not including b.
Reading one character uses an index, just like a list: s[0] is the first, s[-1] the last. A slice copies several at once. Write two indices with a colon between them: s[1:4] takes the characters at 1, 2, and 3 — and stops before 4. The stop index is not included, which is the same fencepost rule you met with range(). Leave a side blank to run to the edge: s[:3] is the first three characters, s[3:] is everything from index 3 onward. And len(s) counts the characters.
Slice and measure s = 'PYTHON' — the steps fade as you master them
s[0]
s[0:3]
s[2:]
s[-1]
len(s)
Now the trait that makes strings different from lists: a string is immutable. You cannot reassign one of its characters. Writing s[0] = 'J' does not edit the text — it raises a TypeError. So every operation that seems to change a string actually returns a new string and leaves the original alone. s.upper() hands back an upper-case copy; s.replace('a', 'o') hands back a copy with the swaps made; s + '!' joins two strings into a third. If you want to keep the result, you must store it — often back in the same name, with s = s.upper().
Why is this true?
Why does s = 'cat' followed by s.upper() still leave s as 'cat'?
Because a string is immutable, upper() cannot change s in place; it returns a new string 'CAT' and leaves the original untouched. Unless you store that result — s = s.upper() — the new string is computed and then discarded.
Searching a string is common enough to have its own word: the in operator. 'TH' in 'PYTHON' is True because those characters appear in order; 'Z' in 'PYTHON' is False. Since a string is a sequence, a for loop walks it one character at a time: for ch in s: binds ch to each character in turn. That is how you rebuild text — loop across the old string, decide what each character becomes, and add the pieces onto a new string you are growing.
A string, then, is a list-like sequence of characters you can index and slice, search with in, and walk with a loop — but never edit in place. To change text is to build new text. With lists holding many values and strings holding text, you have the two collections most programs scan. The next folio puts a loop to work on them: one pass across a sequence, carrying a single memory variable, is enough to find the largest value, count the matches, or search for one.
Practice — new ink and old, interleaved
1.Without looking back: what does a for loop do each pass, and what does range(n) produce?
Each pass, a for loop binds its loop variable to the next item and runs the body once; range(n) produces the numbers 0 up to but not including n.
How close were you? Grade yourself honestly — it sets your review date.
2.x is 3. if x > 5: print('a') / elif x > 1: print('b') / else: print('c'). What is printed?
3.What prints?
``
s = 'hi'
if len(s) == 2:
print('short')
else:
print('long')
``
4.For nums = [11, 22, 33], what is nums[-1]?
5.Why does age = input() followed by print(age + 1) cause an error?
6.name = input() and the person types 42. What type is name?
7.How many times does the body run?
``
count = 0
for ch in 'code':
count = count + 1
print(count)
``
8.Review from folio 2: what is the type of "7", written with quotes?
9.What is len('archive')?
10.For s = 'MARBLE', what is s[2:5]?