Lesson Review
Strings: Indexing, Slicing & Immutability
"Python"s[0]s[-1]s[1:4]s[::-1]"J" + s[1:]
Text is a sequence of characters. Pull out exactly the pieces you need, and learn why you can never change a string in place.
By the end of this lesson you will be able to:
- Construct strings with quotes, escape characters,
str(), +, and *.
- Index a string to get one character (positive and negative indexes).
- Slice a string to extract a substring.
- Explain why strings are immutable, just like tuples, and build new strings instead.
How to use this page: Use Next and Back (or the arrow keys). Type your own words into the string ruler and slicer, run the editors, and try the substring challenge.
Words you will hear today
| Word | What it means | Example |
| character | One letter, digit, space, or symbol | "P", " ", "!" |
| index | The position number of one character | s[0] |
| slice | A piece of a string, made with a colon | s[1:4] |
| substring | A smaller string found inside a bigger one | "gram" in "programming" |
| immutable | Cannot be changed after it is created | strings, tuples, numbers |
| method | A function that belongs to a value, called with a dot | s.upper() |
- Construct, index, slice, immutable.
- Index = one position. Slice = a piece. Immutable = cannot change.
You have used strings since the first day: every message you printed was one. Today we look inside them. A string is a sequence of characters, so everything you learned about indexing and slicing lists and tuples works on strings too. And like tuples, strings can never be changed once they are made.
Strings
A string is a sequence of characters
- A string (
str) is text: letters, digits, spaces, punctuation, and symbols, in order.
- Each character has a position, just like the items in a list or tuple.
- The tutorial: "There is no separate character type; a character is simply a string of size one."
len(s) counts the characters, including spaces.
s = "Hi there!"
print(len(s)) # 9 (the space and ! count)
print(type(s)) # <class 'str'>
print(s[0]) # H (a string of length 1)
| list | tuple | str |
| Holds | any items | any items | characters only |
| Indexing & slicing | Yes | Yes | Yes |
len, in, for | Yes | Yes | Yes |
| Can change items? | Yes | No | No |
Strings belong to the same family as lists and tuples: they are all sequences. Once you know one, you know how to index and slice the others.
Strings are everywhere
Names, passwords, messages, file names, web addresses, and everything a user types with input() are strings. Learning to pull strings apart and build new ones is one of the most useful skills in programming.
Digits in a string are just characters
"2026" is four characters: "2", "0", "2", "6". It is not a number until you convert it with int(). So len("2026") is 4, and "2026"[0] is the string "2".
- String = sequence of characters. No separate char type.
- Same indexing and slicing as lists and tuples.
- Digits in quotes are characters, not numbers.
Think of a string as a row of tiny boxes, each holding one character. Spaces and punctuation get their own boxes too. A single character in Python is just a string with a length of one. And notice the last row of the table: like tuples, strings cannot be changed.
Constructing
Ways to build a string
| Way | Example | Result |
| Double or single quotes | "Hi" or 'Hi' | 'Hi' |
| Quotes inside quotes | "It's", 'Say "hi"' | use the other kind outside |
| Empty string | "" | length 0 |
| Convert | str(42) | '42' |
| Concatenate | "ab" + "cd" | 'abcd' |
| Repeat | "ha" * 3 | 'hahaha' |
| f-string | f"{2 + 3} apples" | '5 apples' |
| Triple quotes | """two lines""" | can span lines |
The tutorial: strings "can be enclosed in single quotes ('...') or double quotes ("...") with the same result."
Choosing the right way to build
- Fixed text: just use quotes.
- Joining a few strings:
+. Remember to add your own spaces.
- Mixing text and numbers: an f-string is the easiest. Anything in
{ } is calculated and turned into text for you: f"{name} has {score} points". No str() needed.
- A repeated pattern:
*, like "-" * 20 for a line.
f-strings can format numbers too: f"{3.14159:.2f}" gives '3.14'. The :.2f means "2 digits after the decimal point."
- Quotes, str(), +, *, f-strings, triple quotes.
- f-strings mix text and values without str().
There are many ways to make a string. Single and double quotes work the same, so pick the one that lets you include the other kind inside. Plus joins strings, star repeats them, and str turns a number into text. Try the option with two strings side by side: Python glues them together automatically.
Constructing
Escape characters and raw strings
A backslash \ starts an escape sequence: two characters in your code that stand for one special character.
| Escape | Means | Length |
\n | new line | 1 |
\t | tab | 1 |
\\ | one backslash | 1 |
\' and \" | a quote that does not end the string | 1 |
The tutorial: "If you don't want characters prefaced by \ to be interpreted as special characters, you can use raw strings by adding an r before the first quote."
Watch out: "C:\new" has no error, but \n becomes a new line! Use r"C:\new" or "C:\\new".
Why do we need escapes?
Some characters cannot be typed directly inside a string. Pressing Enter in the middle of a normal string ends the line of code, and a quote of the same kind ends the string. The backslash tells Python "the next character is special, treat it differently."
Counting escapes
An escape sequence is two characters in your code but one character in the string. len("a\tb") is 3: a, a tab, b. When you index, the escape takes one position: "a\tb"[1] is the tab.
When to use raw strings: mostly for Windows file paths and special search patterns that have lots of backslashes. For everyday text, normal strings are fine.
- \n, \t, \\, \', \" are one character each.
- r"..." turns escapes off.
- An escape is two characters in code, one in the string.
Some characters are hard to type inside quotes, like a new line or a tab. The backslash is Python's way of saying "the next character is special." Each escape sequence counts as one character. Try the file path options to see why raw strings exist.
Indexing
Indexing: one character at a time
word = "Python"
# P y t h o n
# 0 1 2 3 4 5
# -6 -5 -4 -3 -2 -1
print(word[0]) # P
print(word[3]) # h
print(word[-1]) # n
print(word[6]) # IndexError: string index out of range
- Same rules as lists: the first character is
[0] and the last is [-1].
- The last positive index is
len(word) - 1.
- Spaces and punctuation have indexes too: in
"Hi there", index 2 is the space.
Type any word, then click a box (or type an index) to see both of its indexes.
How negative indexes work
Python turns a negative index into a positive one by adding the length. For "Python" (length 6), word[-2] is the same as word[6 - 2], which is word[4], the letter o.
That is why [-1] is always the last character, no matter how long the string is. You do not need to know the length.
An index gives one character
The result of word[0] is a string of length 1. You can use it anywhere a string works: word[0].upper(), word[0] == "P", or word[0] + "!".
- s[0] first, s[-1] last, s[len(s) - 1] last.
- Too big: IndexError: string index out of range.
- A negative index i means len + i.
Indexing a string gives one character. Zero is the first, negative one is the last. Type your own name into the ruler and click the boxes. What index is the last letter of your name, counting from the front?
Indexing
Looping through characters, and in
for ch in "hey": # one character at a time
print(ch)
for i, ch in enumerate("hey"):
print(i, ch) # index and character
For strings, in checks for a substring, not just one character:
"gram" in "programming" # True
"G" in "programming" # False (case sensitive)
"x" not in "programming" # True
Every character has a number code. ord("A") is 65 and chr(65) is 'A'. This is why capital letters sort before lowercase: ord("Z") is 90 but ord("a") is 97.
Value loop or index loop?
for ch in s: when you only need each character.
for i, ch in enumerate(s): when you also need its position, for example "find where the first space is."
for i in range(len(s)): also works and gives you just the index; use s[i] to get the character.
A loop that builds a new string
word = "banana"
result = ""
for ch in word:
if ch != "a":
result += ch
print(result) # bnn
Start with an empty string and add only the characters you want. This is the string version of the accumulator pattern.
- for ch in s: each character.
- in checks substrings.
- Build a new string: start with "" and add with +=.
A for loop over a string visits each character in order. The in keyword is extra powerful for strings: it can find a whole substring, not just a single letter. Try the vowel counter, which combines a loop with in.
Slicing
Slicing: extracting a substring
s[start:stop] gives a new string from start up to, but not including, stop.
Slice of s = "Python" | Result |
s[0:3] or s[:3] | 'Pyt' (first 3) |
s[2:5] | 'tho' |
s[3:] | 'hon' (from index 3 to the end) |
s[-3:] | 'hon' (last 3) |
s[:-1] | 'Pytho' (all but the last) |
s[0:100] | 'Python' (no error) |
s[5:2] | '' (empty, no error) |
The tutorial: "the start is always included, and the end always excluded. This makes sure that s[:i] + s[i:] is always equal to s."
Think of indexes as the gaps between letters
| P | y | t | h | o | n |
0 1 2 3 4 5 6
If you picture each index as the edge before a character, a slice s[1:4] is everything between edge 1 and edge 4: yth. This picture also explains why s[6] is an error (there is no character after edge 6) but s[6:] is fine (it is just empty).
Slices never crash. Python quietly trims a slice to fit the string. Indexes are strict; slices are forgiving. So s[10] is an IndexError, but s[10:11] is just ''.
Quick length check: s[2:5] has 5 − 2 = 3 characters.
- Start in, stop out. Length = stop - start.
- Out-of-range slices are fine.
- Picture indexes as edges between characters.
- Indexes are strict; slices are forgiving.
Slicing works exactly like it did for lists, but the result is a string. Remember the rule: start is in, stop is out. And slices are forgiving: if you go past the end, you just get what is there, with no error.
Slicing
Steps and reversing
Slice of s = "Python" | Result |
s[::2] | 'Pto' (every other) |
s[1::2] | 'yhn' |
s[::-1] | 'nohtyP' (reversed) |
s[4:0:-1] | 'ohty' |
Palindrome check: a word that reads the same backward, like "level," passes word == word[::-1].
Type a word and check whether it is a palindrome.
How a negative step reads
With a negative step, the slice walks backward, so the start should be the larger index. s[4:0:-1] starts at index 4 (o) and goes down to, but not including, index 0: o, h, t, y.
Leaving both ends blank with [::-1] means "start at the end and go all the way to the beginning," which reverses the whole string.
Palindromes with spaces and capitals
"Race car" fails the simple check because of the capital R and the space. Clean it first:
clean = "Race car".lower().replace(" ", "") # 'racecar'
print(clean == clean[::-1]) # True
- [::2] every other. [::-1] reversed.
- Negative step: start at the bigger index.
- Clean with lower() and replace() before a palindrome check.
A third number in the slice is the step. A step of negative one walks backward through the whole string, which reverses it. That gives a one-line palindrome test. Try racecar, level, and your own name.
Practice
String Slicer
Type any text, pick a start, stop, and step, and watch which characters end up in the slice. The small numbers show the order. Nothing is recorded.
Things to try in the slicer
- Leave start and stop blank and set step to 2. Which characters are picked?
- Set a start that is bigger than the stop with a step of 1. Why is the result empty?
- Now keep the same start and stop but make the step −1. What changed?
- Use negative numbers for start and stop, like −4 and −1. Which characters do you get?
- Try your full name; slice out your first name with one slice.
Type your full name. Can you slice out just your first name? Just your last name? Your name backward? Notice that spaces are characters too, so they have indexes and can end up in a slice.
Immutability
Strings are immutable
The tutorial: "Python strings cannot be changed — they are immutable. Therefore, assigning to an indexed position in the string results in an error."
| Try to... | Code | Python 3.13 says |
| replace a character | s[0] = "J" | TypeError: 'str' object does not support item assignment |
| delete a character | del s[0] | TypeError: 'str' object doesn't support item deletion |
| append | s.append("!") | AttributeError: 'str' object has no attribute 'append' |
The tutorial again: "If you need a different string, you should create a new one." s = "J" + s[1:] builds a new string and points the name s at it.
What "reassigning" really does
s = "J" + s[1:] does not change the old string. Step by step:
s[1:] makes a new string "ython".
"J" + "ython" makes another new string, "Jython".
s = ... moves the name s to point at "Jython". The old "Python" is untouched; if nothing else uses it, Python cleans it up.
So the variable can change what it refers to, even though the string itself never changes.
- No item assignment, no del, no append.
- Build a new string and reassign the name.
- The name moves to a new string; the old string never changes.
Just like a tuple, a string cannot be changed once it exists. You cannot replace a letter, delete one, or append to it. What you can do is build a brand new string out of pieces of the old one, and reuse the same variable name. Try each option, then try the two that work.
Immutability
Why are strings immutable?
The Python Design FAQ answers this question directly:
"One is performance: knowing that a string is immutable means we can allocate space for it at creation time, and the storage requirements are fixed and unchanging. This is also one of the reasons for the distinction between tuples and lists."
"Another advantage is that strings in Python are considered as 'elemental' as numbers. No amount of activity will change the value 8 to anything else, and in Python, no amount of activity will change the string 'eight' to anything else."
What that means for you
- Like numbers:
x = 8 then x = x + 1 does not change 8; it makes 9. Strings work the same way.
- Safe to share: two names can point to one string, and neither can change it behind the other's back.
- Dictionary keys: only immutable values can be keys, so strings (like tuples) make great keys.
- Same as tuples: a fixed size and fixed contents, chosen when it is created.
Lists are the mutable sequence. Tuples and strings are the immutable ones.
A simple way to see it
Think of a string like a printed word on a sticker. You cannot erase one letter, but you can print a new sticker and put the label (your variable name) on it instead. Lists are more like a whiteboard: you can erase and rewrite parts.
Why it matters in practice: because nothing can change a string, you never have to worry that some other part of your program will quietly change your text. That makes programs easier to reason about.
- Performance and "elemental" like numbers.
- Same reasoning as tuples.
- Strings are like stickers; lists are like whiteboards.
Why design it this way? The official answer has two parts. First, speed and memory: Python knows exactly how much space a string needs when it is made. Second, Python treats strings like numbers. The number 8 never turns into 9; you make a new number. Strings work the same way: you never change "eight," you make a new string.
Immutability
"Changing" a string means making a new one
String methods like upper() and replace() never change the original. They return a new string. To keep the result, assign it:
name = "ava"
name.upper() # makes 'AVA' ... then throws it away
print(name) # ava (unchanged!)
name = name.upper() # keep the new string
print(name) # AVA
| Goal | New-string recipe |
| Capitalize the first letter | s = s[0].upper() + s[1:] |
| Replace text | s = s.replace("a", "@") |
| Add to the end | s = s + "!" or s += "!" |
Remove a character at index i | s = s[:i] + s[i + 1:] |
Useful methods that return new strings
| Method | Example | Result |
.lower() / .upper() | "Hi".lower() | 'hi' |
.strip() | " hi ".strip() | 'hi' (spaces removed from both ends) |
.replace(old, new) | "a-b".replace("-", " ") | 'a b' |
.title() | "ava lee".title() | 'Ava Lee' |
And some that give back information instead of a string: "banana".count("a") is 3, "Python".startswith("Py") is True, and "a,b,c".split(",") gives the list ['a', 'b', 'c'].
Cleaning user input: answer = input("Yes or no? ").strip().lower() removes extra spaces and capital letters in one line, so " YES " becomes "yes".
- Methods return new strings. Assign to keep them.
- .strip().lower() cleans user input.
This is the most common string bug: calling upper or replace and expecting the original to change. It cannot change, because strings are immutable. The method hands you a new string, and if you do not store it, it disappears. Compare the options with and without the assignment.
Slicing
Real-world slicing: extracting pieces
Combine find() (which gives the index of a substring, or -1 if it is missing) with slicing:
full = "Ava Lee"
space = full.find(" ") # 3
first = full[:space] # 'Ava'
last = full[space + 1:] # 'Lee'
email = "ava@school.org"
at = email.find("@")
user = email[:at] # 'ava'
domain = email[at + 1:] # 'school.org'
Fixed-format text is even easier: in "954-555-0123", the area code is always [:3] and the last four digits are [-4:].
Watch out when find() fails
If the text is not found, find() returns -1, and -1 is a valid slice position (the last character). So with email = "avaschool" (no @), email[:at] becomes email[:-1], which is 'avaschoo'. No error, just a wrong answer.
at = email.find("@")
if at == -1:
print("That is not an email address.")
else:
print("User:", email[:at])
Why space + 1? The slice for the last name should start after the space, not on it. Without the + 1, the result would be ' Lee' with a leading space.
- find() + slicing extracts names, usernames, domains.
- Check for find() returning -1 before slicing.
Here is where slicing pays off. Use find to locate a landmark, like a space or an at sign, then slice before it or after it. The plus one skips past the landmark itself. Step through each example and watch the index variables.
Practice
Substring challenge
Type an index or slice in brackets (like [0] or [0:5]) that gets each target from s, then click Check. Practice only; nothing is recorded.
Strategy
- Write the string with the index number under every character, including spaces and punctuation.
- Find the index of the first character you want. That is your start.
- Find the index of the last character you want and add 1. That is your stop.
- For pieces at the end, a negative start with no stop is often easiest:
[-4:].
- Count carefully: the comma and space are characters too.
Write the indexes under the string on scratch paper first. The comma and the space each take a position. For the last two, think about the step.
Debugging
Common mistakes
| Mistake | Code | What happens (Python 3.13) | Fix |
| Changing a character | s[0] = "J" | TypeError: 'str' object does not support item assignment | s = "J" + s[1:] |
| Not saving a method's result | s.upper() | No error. s is unchanged | s = s.upper() |
| Index past the end | s[len(s)] | IndexError: string index out of range | s[-1] |
| Expecting the stop character | "Python"[1:3] for 3 letters | No error. Only 'yt' | [1:4] |
| Quote inside the same quotes | 'It's' | SyntaxError: unterminated string literal (detected at line 1) | "It's" |
| Backslash in a path | "C:\new" | No error. \n becomes a new line | r"C:\new" |
| Joining text and a number | "Score: " + 95 | TypeError: can only concatenate str (not "int") to str | "Score: " + str(95) |
| Using a list method | s.append("!") | AttributeError: 'str' object has no attribute 'append' | s = s + "!" |
| Wrong case in a search | "G" in "programming" | No error. False | Match the case, or use .lower() |
Try these at the prompt:
s = "Python"
s[1:4]
s[::-1]
s[0] = "J"
s.upper()
s
s = "J" + s[1:]
s
The two sneakiest bugs
- Forgetting to save a method's result.
name.upper() on its own line runs, makes a new string, and immediately throws it away. If a string "didn't change," check for a missing name = ....
- Off-by-one in a slice. If a slice is one character short, the stop is probably one too small. Remember: stop is the first index you do not want.
Error type tells you where to look: TypeError ... item assignment means you tried to change a string. IndexError means an index is past the end. AttributeError means you used a method strings do not have (like a list method).
- Immutability errors and the "method result not saved" bug are the big ones.
- A string that "didn't change" usually means the method result was not saved.
Notice how many of these give no error: not saving a method's result, the off-by-one slice, the backslash path, and the case-sensitive search. Python is doing exactly what you wrote, so it is up to you to know the rules.
Practice
Check yourself
Every question uses s = "Python". Choose an answer, then click Check. Practice only; nothing is graded or recorded.
Before answering, write this down:
P y t h o n
0 1 2 3 4 5
-6 -5 -4 -3 -2 -1
Then for a slice, take characters from start up to (but not including) stop, moving by the step.
- Write P y t h o n with indexes 0 to 5 before answering.
Write the six letters of Python with their indexes zero through five before you start. Then each question is just reading your chart.
Summary
Cheat sheet
Build
"Hi" 'Hi' ""
"It's" 'Say "hi"'
"a\nb" r"C:\new"
str(42) # '42'
"ab" + "cd" # 'abcd'
"ha" * 3 # 'hahaha'
f"{n} apples"
Index and slice
s = "Python"
s[0] # 'P'
s[-1] # 'n'
s[1:4] # 'yth'
s[:3] # 'Pyt'
s[-3:] # 'hon'
s[::-1] # 'nohtyP'
len(s) # 6
Immutable
s[0] = "J" is a TypeError
- Make a new string:
s = "J" + s[1:]
- Methods return new strings:
s = s.upper()
- Same idea as tuples; lists are the only one of the three you can change
Remember:
- Indexes start at 0; the last character is
[-1]
- Slices: start is in, stop is out, and a slice never raises an IndexError
- Spaces and punctuation are characters with their own indexes
- You never change a string; you build a new one and reassign the name
Key words to know
string · character · escape sequence · raw string · f-string · index (positive and negative) · slice · step · substring · immutable · method
- Build, index, slice, immutable.
- Know the key words: index, slice, step, substring, immutable, method.
Let's wrap up. A string is a sequence of characters that you can index and slice just like a list or tuple. Like a tuple, it is immutable: you never change a string, you build a new one. Keep that in mind and string bugs become much easier to spot.