Why it matters
Names, messages, file paths, search queries and input lines are all text. In a coding interview, a large share of the questions are about text: is this word a palindrome, are these two words anagrams, which letter appears most often, how do you reverse a sentence. The Python type for text is str, and it appears in almost every program you will write. Two mistakes cause most of the string bugs beginners meet. One is joining text and numbers without converting them. The other is building a long string inside a loop in a way that becomes very slow. This lesson covers both, then uses strings to practise two patterns you will need for the rest of the course: two pointers and counting.
The idea: a string is a row of lockers
A str is a sequence of characters. Python has no separate character type. A single character is simply a str of length 1. The string is an object: a value that comes with built-in actions called methods. You call a method with a dot after the value, as in word.upper(). You will learn to write your own objects in the object-oriented module. For now, treat a string as a value that knows how to answer questions about itself.
Think of a string as a row of numbered lockers. Each locker holds one character. The lockers are numbered from 0, not from 1. The number on a locker is called its index. The first character is at index 0, and the last character is at index len(word) - 1.
word = "Forge"| Index | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| Character | F | o | r | g | e |
The string has five characters, so len(word) is 5, but the last index is 4. Off-by-one errors, where a loop or an index is one too far, are the most common bug in text algorithms. Python also accepts negative indexes, where -1 means the last character, -2 the one before it, and so on. Keep the table in mind whenever you write an index.
Useful operations
Python has many string methods. You need about ten of them in the first month. The program below uses most of them on one word. Read the code and predict each line before you run it.
5
F
or
FORGE
3
-1
True
padded|
F0rge
ForgeHere is what each operation does, with the rule that matters most.
len(word)counts the characters. It is a function, not a method, so you writelen(word). Writingword.length()fails withAttributeError.word[i]returns the character at indexias a one-character string.word[0]is"F".word[a:b]is a slice. It returns the characters from indexaup to, but not including, indexb. The result hasb - acharacters. A slice with a missing start or end uses the beginning or the end of the string.upper()andlower()return a copy with the letters changed.find(text)returns the first index where the text starts, or-1if it does not appear. You must check for-1before using the result as an index. The methodindex(text)raises an error instead of returning-1.text in wordreturnsTrueif the text appears anywhere inside the string.strip()removes spaces at the start and end, but not in the middle.replace(old, new)returns a copy with every occurrence changed.s == ""isTrueonly for the empty string.s.strip() == ""isTruefor an empty string or one with only spaces.list(word)returns the characters as a list. A list is a changeable sequence, and you will use it below to reverse a string.
The last line of the program prints Forge again, which shows the most important rule about strings, covered next.
Strings never change in place
A str is immutable. Once it exists, nothing can change its characters. Methods such as upper and replace build and return a new string. The original stays the same.
Forge
FORGEThe call word.upper() creates a second string "FORGE", and nothing stores it, so Python throws it away. The line loud = word.upper() keeps the new string under the name loud. A beginner who writes word.upper() on a line by itself, expects word to change, and is surprised. Every method that appears to change a string is really returning a new one, so store the result.
Immutability has a good side too. Two names can share one string safely, because neither one can change it under the other. The bad side is cost. Each new string takes time to build and memory to store. That cost decides how you should build long text, and a later section shows it.
Joining text with +
The + sign joins two strings. It does not join a string with a number. Python does not turn the number into text for you, so the join stops with an error.
Age: 20
Age: 20The line print(1 + 2 + "3") fails, and its error is in the mistakes section. The first two lines show the two standard fixes. str(age) converts the number to text, and an f-string builds the text with the value placed inside the braces. The f-string is the usual choice, because it reads more clearly.
Joining in a loop is where the cost appears. The next program builds the same ten-character string in two ways.
ababababab
ababababab
True
10The first loop uses text = text + "ab". Each pass creates a new string that is one longer, copies every character of the old one into it, and then adds two more. With five passes the copying is small. With a hundred thousand passes the copying adds up to billions of character copies. The total work grows with the square of the number of passes, so the loop becomes too slow long before the answer is wrong. CPython, the usual Python interpreter, can sometimes extend a string in place, but that is an internal detail you should not rely on.
The second loop collects the pieces in a list and joins them once at the end. list.append is cheap, and "".join(pieces) builds the final string in one pass, so the time is proportional to the total length. Use this pattern whenever a loop adds to a string. The method join takes a list of strings and puts a separator between them. The separator here is the empty string.
The interview version of this rule is short: "joining strings in a loop with plus is quadratic in the number of pieces, so I collect the pieces in a list and call join once at the end." Say it that way, and give the reason.
Comparing strings: == compares values, is compares objects
The == operator checks whether two strings contain the same characters. The keyword is checks whether two names point to the same object. These are different questions, and mixing them up is a common string bug.
True
False
True
True
True
TrueThe first line is True because == compares the characters. The second line is False because the two strings were built at run time, so they are two different objects. Always use == for text. The is keyword is for checking whether a value is None, which you will meet in the functions lesson. The third line shows the usual case-insensitive comparison: convert both sides to lowercase first. casefold() is a stronger form for languages with unusual capital letters.
Python compares strings by their code numbers, letter by letter. "apple" < "banana" is True because a comes before b. "cat" > "car" is True because the first difference is t against r, and t has a larger code. There is no compareTo method. In a sort you pass the strings straight to sorted, and Python uses the same rule.
Two strings with the same characters can be the same object in CPython, because it shares some short strings. That is a storage detail, so the rule stays simple: compare text with ==.
Indexes and the errors you will see
When you ask for a character outside the valid range, Python stops the program. Here is the message for a five-letter word and an index of 5.
print("Forge"[5])Traceback (most recent call last):
File ".../index.py", line 1, in <module>
print("Forge"[5])
~~~~~~~^^^
IndexError: string index out of rangeThe message says the index was out of range. The valid indexes are 0 to 4. You avoid this error by writing loops that stop before the length, such as for i in range(len(s)), and by writing s[-1] when you need the last character. Slices are more forgiving. "Forge"[2:9] returns "rge" and does not raise an error, because Python clips a slice to the string. Index errors do not behave this way, so do not assume that a slice and an index fail in the same way.
A second error is about None. Python uses None for "no value". If a variable holds None, calling a string method on it fails:
name = None
print(name.lower())Traceback (most recent call last):
File ".../none.py", line 2, in <module>
print(name.lower())
^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'lower'Read it in this order: the last line gives the kind of problem, NoneType is the type of None, and the line above it shows which call failed. Find that line, and ask where name got its value. A None string usually comes from a function that could not find an answer and returned nothing, or from a variable nobody set.
Two pointers: the palindrome check
A palindrome is a word that reads the same backwards, such as racecar. The direct check compares the first character with the last, the second with the second-to-last, and so on, until the two positions meet in the middle. Each position is an index, and the two indexes move towards each other. Programmers call this the two-pointers pattern.
The rule in code is simple. Start left at 0 and right at the last index. While left is less than right, compare the two characters. If they differ, the word is not a palindrome, and you stop. If they match, move left one step right and right one step left. If the loop ends without a difference, the word is a palindrome.
'racecar' True
'forge' False
'' True
'a' True
'abba' TrueRead while left < right as "repeat while the markers have not met", and read if as "when this is true, do this". The repr function shows the string in quotes, so the empty string is visible. The if and while statements are covered in the conditionals and loops lessons.
The check has two edge cases worth naming. An empty string gives right = -1, so left < right is false at the start and the answer is "palindrome", which is correct, because the empty string reads the same both ways. A single letter also gives left equal to right at the start, and the loop does not run, so it is a palindrome too. Interviewers ask about these, and a good answer names them before they do.
The cost is one pass over the string, so the time is proportional to the length n. The extra space is two integers, which does not grow with the input. Write both facts when you explain the solution: time O(n), space O(1).
Counting characters
Many string questions ask how often each character appears. Anagram checks, the most frequent letter and duplicate detection all start with counting. One tool for counting is a list of counters, one for each possible letter. A lowercase English letter maps to a number from 0 to 25 by subtracting the code of "a". That works because the codes of a to z are consecutive. The function ord gives the code of a one-letter string.
Here is the idea with the word banana. The letter b has code 98, so ord("b") - ord("a") is 1, and counts[1] is the counter for b. The letter n gives 13, and a gives 0.
a: 3
b: 1
n: 2Trace the first counts. The list starts with 26 zeros. The first character b makes counts[1] equal to 1. The next character a makes counts[0] equal to 1. The third character n makes counts[13] equal to 1. The fourth is another a, so counts[0] is 2, and so on. The second loop walks the list from 0 to 25 and prints the counters that are not zero. The expression chr(ord("a") + k) turns the index back into a letter, which is the reverse of the subtraction.
The [0] * 26 form creates a list of 26 zeros. You will study lists in depth in the arrays module. For now, read counts[ord(ch) - ord("a")] += 1 as "add one to the counter for this letter".
For a general count, when the characters are not just lowercase letters, use a dictionary. The standard library class collections.Counter does the same job for any characters:
Counter({'a': 3, 'n': 2, 'b': 1})
3The Counter prints its counts with the most frequent letter first, and the lookup ["a"] returns 3 for a letter that is present. A missing letter returns 0, so there is no error for a letter that never appears.
Anagrams: two ways to check
Two words are anagrams if they contain the same letters with the same counts, in any order. listen and silent are anagrams. The simplest correct method sorts the letters of both words and compares the sorted results. If the sorted lists match, the words are anagrams.
True
False
FalseThe third line is False because D and d are different characters. Lowercase both words first if the problem ignores capitals: is_anagram("Dusty".lower(), "study") is True.
Sorting takes time proportional to n times the logarithm of n, which is fast enough for most inputs. The counting method from the previous section takes time proportional to n, and it is the better answer when the interviewer asks you to go faster. Both methods are correct. Say both, state the complexity of each, and then pick the one that matches the constraints. This is how you show that you know the trade-off rather than memorising one answer.
Reversing a string
Because strings are immutable, you cannot change their characters directly. To reverse a string, you can slice it with a step of -1, or you can build a list, swap the characters from the two ends, and join the list at the end. The swap uses the two-pointers pattern again.
egroF
egroF
ForgeThe line letters[left], letters[right] = letters[right], letters[left] swaps two values in one step. Python evaluates the right side first, so both values are read before either one is written. The original word is still Forge, because the list is a new object. The slice word[::-1] is the shortest way to reverse a string, and it is the form most Python code uses. The two-pointers version shows the pattern you need for the interview question that asks you to reverse by hand.
Splitting a line into words
Many inputs are one line of words separated by spaces. The split method cuts a string at each separator and returns the pieces in a list. Read parts[0] as "the first piece" and len(parts) as "how many pieces".
Words: 3
First: one
Last: three
['a', '', 'b']
['a', 'b']The string "a b" has two spaces between the letters. split(" ") cuts at each space, so the double space produces an empty piece between a and b. This is the most common surprise with split. Calling split() with no argument splits on any run of whitespace and ignores empty pieces. For messy input, use the form without an argument, or state the assumption that the input has single spaces.
The first unique letter
A common interview question asks for the first character that appears exactly once. The counting method gives the answer in two passes. The first pass fills the counts. The second pass walks the string from the left and returns the first letter whose count is 1.
First unique letter: w at index 1
No unique letterFor the input swiss, the counts are s = 3, w = 1 and i = 1. Walking from the left, s has count 3, so it is skipped. The next letter w has count 1, so the function returns at index 1. For the input aabb, every letter has count 2, the loop finishes without a match, and the function returns the message for no unique letter.
The function enumerate gives each character with its index, so the loop does not need range(len(text)). Both passes take time proportional to n, so the total is O(n). The counter needs space for the distinct characters, which is at most the number of characters in the string.
Unicode, and why a character is a number
A character in Python is a Unicode code point, which is a number from 0 to 1114111. Unicode is a standard table that gives a number to each character in most of the world's writing systems, including Devanagari. The first 128 values match the older ASCII table, so "A" is 65 and "a" is 97 in both.
This is why ord(ch) - ord("a") works and why chr(ord("a") + 1) gives b. The arithmetic happens on the numbers behind the characters. Python's len counts code points, so one emoji has length 1. Some visible characters are built from two or more code points, and then len counts more than what you see on screen. The next program shows the difference.
1 1 2
65 97 aIn the output, the first "é" is one code point, and the second "é" is an e followed by a combining accent, which is two code points. Interview questions on strings usually assume plain ASCII letters, and it is good practice to say so.
Interview questions about strings
These questions come up often. Practise each answer out loud, and include the complexity.
Why is s == t the right test for text in Python? Because == compares the characters. The keyword is compares object identity, and two equal strings built separately are different objects. A good answer adds that is is right only for checking None.
What is the time complexity of len(s)? O(1), because a string stores its length. You can use it in a loop condition without worrying about the cost.
What is the time complexity of s[a:b]? O(b - a), because the new string copies the characters it returns. A solution that takes a slice inside a loop over all positions can become O(n squared) without you noticing.
How do you reverse a string in Python? With s[::-1]. A reversal by hand with two pointers, as in the example above, is also accepted, and it shows the pattern.
Is a string immutable, and why does that matter? Yes. Immutability makes strings safe to share, for example as keys in a dictionary. The cost is that every change makes a copy, so loops that change text should collect pieces in a list and join them once.
How would you check whether a string has all unique characters? Use a set of the characters in O(n) on average, or sort the characters in O(n log n), or use a counter array for lowercase letters in O(n). Ask about the alphabet first, because that decides which method you choose.
How do you check whether "erbottle" is a rotation of "waterbottle"? Check that the lengths match first, then check that the first string appears inside the second string joined with itself. The text waterbottlewaterbottle contains erbottle, so the answer is yes. The length check matters, because without it a short word can appear inside a longer string and give a false yes.
Debugging walkthrough: a palindrome check that fails
A teammate writes this palindrome check and reports that aa is not a palindrome.
def is_palindrome(word):
left = 0
right = len(word)
while left < right:
if word[left] != word[right]:
return False
left += 1
right -= 1
return True
print(is_palindrome("aa"))Run it and the output is a traceback, not False:
Traceback (most recent call last):
File ".../palindrome.py", line 11, in <module>
print(is_palindrome("aa"))
~~~~~~~~~~~~~^^^^^^
File ".../palindrome.py", line 5, in is_palindrome
if word[left] != word[right]:
~~~~^^^^^^^
IndexError: string index out of rangeFollow the steps. First, reproduce the bug on the smallest input. aa has length 2. Second, check the starting values. right is set to len(word), which is 2, not len(word) - 1, which is 1. Third, trace the first comparison: word[0] is a, and word[2] is outside the string, so Python raises IndexError. The teammate sees an error, not a wrong answer, and might blame the loop. Fourth, find the cause: right starts one past the last index. Fifth, fix it with len(word) - 1. Sixth, test the edge cases again: an empty string, a single letter and an even-length word.
The lesson of this walkthrough is that off-by-one errors are found by testing the smallest and largest valid inputs, not by reading the code once. Check the first index, the last index and one step past each.
What happens in memory
A variable does not hold a string's characters itself. It holds a reference, which is a pointer to the object in memory. Think of a reference as a sticky note with a locker number written on it. The note is on your name, and the locker is the object, which holds the characters and the length.
If you write c = a, Python copies the note, not the locker. Now a and c point to the same object, so a is c is True. Because strings are immutable, nothing can change the object through either name, so sharing is safe. Lists, which you will meet in the arrays module, can change in place. For lists, a change through one name is seen through the other, and that is the main reason to be careful with copies.
The memory view also explains the cost of text = text + "ab". Each pass creates a new locker, copies the old contents into it and then drops the old one. The old lockers become garbage, which means memory the program no longer uses. Python frees them as soon as no name points to them. A list of pieces, joined once, avoids the pile of discarded copies.
Common mistakes
These are the mistakes beginners make with strings. Each one comes with the message you will see or the wrong answer you will get.
- Joining text and a number.
1 + 2 + "3"givesTypeError: unsupported operand type(s) for +: 'int' and 'str'. Usestr(1 + 2) + "3"or an f-string. Also"1" + 2givesTypeError: can only concatenate str (not "int") to str. - Assuming a method changes the string.
word.upper()alone does nothing useful. Writeword = word.upper()or store the result in a new name. - Calling
length()on a string."Forge".length()givesAttributeError: 'str' object has no attribute 'length'. Writelen("Forge"). - Index past the end.
"Forge"[5]givesIndexError: string index out of range. The valid indexes are 0 tolen(word) - 1. - Building strings in a loop with +. It works, but it slows down sharply for long inputs. Collect the pieces in a list and call
"".join(pieces). - Using
findand forgetting-1.word.find("z")returns -1, andword[-1]silently gives the last character. Check the result before you use it as an index. - Using
indexwhere you meantfind."abc".index("z")givesValueError: substring not found. Usefindwhen a missing value is normal. - Calling a method on None.
None.lower()givesAttributeError: 'NoneType' object has no attribute 'lower'. Find where the variable was given the value. - Mixing up a character and a string.
"a"is a one-character string, andord("a")is the number 97."a" * 3is"aaa", but"a" - 1is an error.
Practice
Attempt each exercise on your own first. Use input() for the ones that read text, and type the sample values when the program asks for them.
- Read a word, then print its last character. Use
lenand indexing. Test it on a single-letter word. - Predict the output of
print("a" + str(1) + str(2))andprint(1 + 2 + "a"). Then run them. The second one fails. Explain why. - From
"Programming", print"gram"using a slice. Hint: count the indexes from 0. - Read a sentence and print how many spaces it contains. Use a loop over the characters and compare each one with
" ". - Read a word and print it reversed, without using a slice. Use a list and the two-pointer swap from the reversal example.
- Read two words and print
sameif they are equal ignoring case, ordifferentotherwise. Uselowerand==. - Read a word and print
yesif it contains the lettere, usingin, andnootherwise. - Build the string
1 2 3 4 5with numbers separated by single spaces, using a list andjoin. Then print it. - Read a word and print the number of vowels in it, counting
a,e,i,oanduin lowercase. - Explain, in two sentences, why
text = text + "x"inside a loop of 100000 passes is slower thanpieces.append("x")followed by onejoin. - Read a word and print
Trueif the first and last characters are the same letter. Make sure a one-letter word is handled. - Write a program that reads one line and prints the most frequent lowercase letter. If there is a tie, print the letter that comes first alphabetically.
Each exercise has an answer below a "Show the answer" line. Try each one first, then read the answer.
- The last character is
word[-1], orword[len(word) - 1]. For a one-letter word both give that letter.
Show the answer
word = input()
print(word[-1])- The first prints
a12, because the pieces are joined as text. The second fails, because1 + 2is computed first, and then3 + "a"mixes a number and text.
Show the answer
print("a" + str(1) + str(2))
print(1 + 2 + "a")The output is a12, then a TypeError about int and str.
- The slice is
"Programming"[3:7]. Index 3 isgand index 7 is themaftergra, so the result isgram.
Show the answer
print("Programming"[3:7])- Loop over the characters and count when the character is a space.
Show the answer
text = input()
count = 0
for ch in text:
if ch == " ":
count += 1
print(count)- Build a list from the word, then swap the characters from the two ends.
Show the answer
word = input()
letters = list(word)
left = 0
right = len(letters) - 1
while left < right:
letters[left], letters[right] = letters[right], letters[left]
left += 1
right -= 1
print("".join(letters))- The lowercase forms of the two words are compared.
Show the answer
a = input()
b = input()
if a.lower() == b.lower():
print("same")
else:
print("different")- The
inoperator returnsTrueorFalse, so theifreads well.
Show the answer
word = input()
if "e" in word:
print("yes")
else:
print("no")- Collect the numbers as text in a list, then join them with a space.
Show the answer
numbers = [str(i) for i in range(1, 6)]
print(" ".join(numbers))The output is 1 2 3 4 5. The list comprehension is a short way to write a loop that builds a list.
- Count each vowel with a membership test.
Show the answer
word = input()
count = 0
for ch in word:
if ch in "aeiou":
count += 1
print(count)The test ch in "aeiou" is True for any one of the five letters.
-
Each
text + "x"copies the whole string into a new one, so the total copying grows with the square of the number of passes.appendadds to a list in constant time, and onejoinat the end copies each character once. -
Compare the first and last characters. For a one-letter word both indexes are 0, so the answer is
True.
Show the answer
word = input()
print(word[0] == word[-1])- Count the letters with a
Counter, find the largest count, and scan fromatozso that the first letter with the largest count wins the tie.
Show the answer
from collections import Counter
text = input()
counts = Counter(text)
best = None
for ch in "abcdefghijklmnopqrstuvwxyz":
if best is None or counts[ch] > counts[best]:
best = ch
print(best)The scan goes from a to z and only replaces best when a count is strictly larger, so the earliest letter wins a tie.
What you now know
- A
stris an immutable object. Indexes start at 0, the last index islen(s) - 1, and a slices[a:b]returns the characters fromaup tob. - Methods never change a string. They return a new one, so store the result.
+joins two strings. It does not join a string with a number. Use an f-string orstrto convert the number first.- Use
==to compare text andlower()to ignore capitals. Useisonly to check forNone. - Build long text by collecting pieces in a list and calling
"".join(pieces)once. - The two-pointers pattern checks palindromes and reverses text in time O(n) and extra space O(1).
- Counting with a list of 26 counters, using
ord(ch) - ord("a")as the index, solves anagram and frequency questions.Counterdoes the same for any characters.