Strings in Python
A Python string is an immutable sequence of characters. You access characters with zero-based indexing (fruit[0]), extract ranges with slicing (fruit[1:4]), and use built-in methods like .upper(), .find(), and .replace() to work with text.
Most of the data you meet early in Python is numbers — integers, floats, results of calculations. But a huge share of real programming is text: names, messages, passwords, sentences, entire files. In Python, all of that text lives inside a string.
This page goes past "strings are things you print" and treats them as what they really are — sequences you can index, slice, search, and rebuild. Pair it with Data Types in Python for where str fits among Python's other built-in types.
What you will learn
By the end you can:
- Index into a string and explain why counting starts at zero
- Use
len()and negative indices without off-by-one mistakes - Slice out any chunk of a string with
[n:m] - Explain why strings are immutable in Python
- Search, count, and compare strings, and use core string methods
A string is a sequence
A string is not really "one thing" — it is a sequence of individual characters, lined up one after another. Because it is a sequence, you can reach in and pull out any single character using square brackets:
fruit = 'banana'
letter = fruit[1]
print(letter)You might expect this to print b, the first letter. It actually prints a — one of programming's most disorienting moments for almost every beginner. In Python, indexing starts at zero, not one. Think of the index as an offset from the beginning, not a position you count on your fingers.
fruit[0] # 'b'
fruit[1] # 'a'
fruit[2] # 'n'
fruit[3] # 'a'
fruit[4] # 'n'
fruit[5] # 'a'Zero steps from the start means you have not moved at all — you are already standing on the first character. The index must always be a whole number; a decimal is refused outright:
fruit[1.5]
# TypeError: string indices must be integers, not floatlen() and negative indices
The built-in len() function tells you exactly how many characters a string holds:
fruit = 'banana'
len(fruit) # 6Six characters, indexed 0 through 5. This sets up a classic trap: since indexing starts at 0, the last character sits at index 5, not 6.
last = fruit[len(fruit)] # IndexError: string index out of range
last = fruit[len(fruit) - 1] # 'a' (correct)Python gives you a cleaner fix for this exact problem — negative indices, which count backward from the end:
fruit[-1] # 'a' (last character)
fruit[-2] # 'n' (second to last)
fruit[-6] # 'b' (first character)Walking through a string with a for loop
Going through a string one character at a time is called traversal. A while loop can do it, but it requires managing an index by hand:
index = 0
while index < len(fruit):
letter = fruit[index]
print(letter)
index = index + 1A for loop does the same thing far more cleanly — no index variable, no off-by-one risk:
for char in fruit:
print(char)Each pass, char automatically becomes the next character in line, and the loop ends on its own once nothing is left. Traversal combines naturally with concatenation (joining strings with +):
prefixes = 'JKLMNOPQ'
suffix = 'ack'
for letter in prefixes:
print(letter + suffix)
# Jack
# Kack
# Lack
# Mack
# Nack
# Oack
# Pack
# Qack(Oack and Qack are not real English spellings — this simple loop does not know about the special-case rules for O and Q. The point is seeing how naturally a loop builds new strings by gluing pieces together.)
String slices — cutting out a piece
Just like a single index gets one character, a slice gets an entire chunk at once:
s = 'Monty Python'
s[0:5] # 'Monty'
s[6:12] # 'Python'[n:m] means every character from index n up to — but not including — index m. This has a useful side effect: s[0:5] gives exactly 5 characters, and s[5:12] picks up right where it left off, clean and chainable.
Leave out the first number to start from the beginning; leave out the second to run to the end:
fruit = 'banana'
fruit[:3] # 'ban' (start through index 2)
fruit[3:] # 'ana' (index 3 to the end)
fruit[:] # 'banana' (the whole string)If the first index is greater than or equal to the second, you get an empty string — a valid string with zero characters:
fruit[3:3] # ''Strings are immutable
You might expect to change a single character the way you originally assigned one, by putting the bracket on the left of =. Python refuses:
greeting = 'Hello, world!'
greeting[0] = 'J'
# TypeError: 'str' object does not support item assignmentStrings in Python are immutable — once created, they can never be modified in place. What you build instead is a brand-new string from pieces of the old one:
greeting = 'Hello, world!'
new_greeting = 'J' + greeting[1:]
print(new_greeting) # 'Jello, world!'The original greeting is untouched; new_greeting is a completely separate string. This is not a limitation — strings that cannot secretly change underneath you are safer and easier to reason about.
Searching inside a string
Here is a function that finds where a character first appears — the reverse of indexing: give it a character, get back a position.
def find(word, letter):
index = 0
while index < len(word):
if word[index] == letter:
return index
index = index + 1
return -1If the character never appears, the function returns -1 — a common convention for "not found." Notice the return inside the loop: the function exits the moment a match is found, with no need to keep checking. This pattern — walk a sequence, return the moment you find what you want — is called a search, and you will reuse this shape constantly.
Looping and counting
Counting how many times something appears is another everyday pattern:
word = 'banana'
count = 0
for letter in word:
if letter == 'a':
count = count + 1
print(count) # 3count starts at zero and increases by one every time 'a' shows up. This pattern is called a counter, and you will use it for counting characters, matches, or anything that satisfies a condition.
String methods — functions that belong to strings
Everything so far has used function_name(argument). Python also has methods — functions permanently attached to a specific type, called with a dot instead of standing alone:
word = 'banana'
new_word = word.upper()
print(new_word) # 'BANANA'Instead of upper(word), you write word.upper() — this is called invoking a method on an object. Python's built-in .find() method does what the handwritten find function above does, and more:
word = 'banana'
word.find('a') # 1 (first occurrence)
word.find('na') # 2 (whole substrings, not just single letters)
word.find('na', 3) # 4 (start searching from index 3)
word.find('b', 1, 2) # -1 (only search between index 1 and 2)A few more string methods worth knowing right away:
word.upper() # 'BANANA' — all uppercase
word.lower() # 'banana' — all lowercase
word.strip() # removes whitespace from both ends
word.replace('a', 'o') # 'bonono' — replaces every 'a' with 'o'The full list lives in Python's own documentation, but .strip() and .replace() show up constantly and are worth remembering early.
The in operator
in checks whether one string appears inside another, returning True or False:
'a' in 'banana' # True
'seed' in 'banana' # False
'nan' in 'banana' # True (whole substrings work too)This makes some code read almost like plain English:
def in_both(word1, word2):
for letter in word1:
if letter in word2:
print(letter)Read it out loud: "For each letter in word1, if the letter is in word2, print it." That is almost exactly what the code does.
Comparing strings
==, !=, <, and > all work on strings. Python compares them alphabetically (technically by Unicode value, but alphabetical is the right mental model for now):
if word == 'banana':
print('All right, bananas.')
if word < 'banana':
print('Your word comes before banana.')
elif word > 'banana':
print('Your word comes after banana.')
else:
print('All right, bananas.')Debugging: off-by-one errors with indices
Index bugs are one of the single most common sources of errors in Python. The most frequent mistake: being off by one — starting or ending your counting in the wrong place.
def is_reverse(word1, word2):
if len(word1) != len(word2):
return False
i = 0
j = len(word2) # bug hiding here
while j > 0:
if word1[i] != word2[j]: # and here too
return False
i = i + 1
j = j - 1
return TrueCalling is_reverse('pots', 'stop') throws an IndexError. j starts at len(word2), which is 4 — but valid indices for a 4-character string only run 0 through 3. The fix: j = len(word2) - 1.
The debugging habit worth keeping for life: print right before the line that crashes, to see exactly what your variables hold at that moment.
while j > 0:
print(i, j) # what are these values right now?
if word1[i] != word2[j]:
...That output would immediately reveal j sitting at 4 — out of range for a 4-character word. Once you know the exact values that caused the crash, figuring out why is usually straightforward.
Common mistakes
- Expecting
fruit[1]to be the first character (it's the second — indexing starts at 0) - Using
fruit[len(fruit)]to grab the last character instead offruit[-1]orfruit[len(fruit) - 1] - Trying to assign into a string like
greeting[0] = 'J'— strings are immutable - Forgetting slices exclude the end index —
s[0:5]stops before index 5 - Comparing strings without normalizing case first
Why this matters
Strings show up in nearly every real program — parsing input, validating data, building messages, processing files. Understanding them as indexable, sliceable, immutable sequences — not just "text you print" — is what lets you manipulate real-world data with confidence.
Common questions
Why does Python string indexing start at 0?
Python indices represent an offset from the start of the sequence, not a position count. Index 0 means zero steps from the beginning, so the first character is at index 0, the second at index 1, and so on.
How do you slice a string in Python?
Use s[n:m] to get characters from index n up to but not including index m. Leaving out n starts from the beginning; leaving out m runs to the end, e.g. s[:3] or s[3:].
Are strings mutable or immutable in Python?
Strings are immutable. You cannot change a character in place (greeting[0] = 'J' raises a TypeError). Instead, you build a new string from pieces of the old one.
How do you find a substring in a Python string?
Use the .find() method, e.g. word.find('na'), which returns the index of the first match or -1 if not found. The in operator ('na' in word) returns True or False instead.
How do you check if a string contains another string in Python?
Use the in operator: 'seed' in 'banana' returns False, while 'nan' in 'banana' returns True. This works for whole substrings, not just single characters.
Why does string comparison give unexpected results in Python?
Python compares strings by Unicode value, and all uppercase letters rank before all lowercase letters. So 'Pineapple' < 'banana' is True. Convert both sides with .lower() before comparing to avoid this.
Explore
Related topics
Keep going — these sit next to this concept in a real learning path.
Data Types in Python
Where str fits among Python's built-in types.
The input() Function in Python
input() always returns a string — this page explains what that means.
Operators and Operands in Python
How + and comparison operators behave on strings.
Types, Values, and Errors in Python
The type() function and how errors like TypeError work.
Lists in Python
Mutability, list methods, map/filter/reduce, and the aliasing trap.
Dictionaries in Python
Key-value pairs, the histogram pattern, and memoization.
Tuples in Python
Immutability, unpacking, *args, zip(), and DSU sorting.
Python course hub
All free Python explainers and the path into Agentic practice.