Embercode
✕
StringsLesson 1 of 16

46 min read

Python strings, basics and immutability

Why it matters

Almost every program handles text. A login form reads a user name, a search box compares two words, a file parser splits a line into fields, and a report prints a sentence built from parts. In Python, text lives in an object of type str, and nearly every interview question about text starts with one. The rules that surprise beginners are small: positions start at 0, == compares the characters while is compares object identity, and a method that seems to change a string actually returns a new one. If you know these rules well, the harder string problems in this module become a matter of patterns rather than guesswork.

This lesson is the foundation for the other three. You will learn what a str is, how its positions work, which methods you will use every day, why immutability matters and how to read the error messages that appear when you get something wrong. You need no earlier Python beyond the idea of a variable and a print call.

The idea: text is a row of numbered boxes

A string is a sequence of characters. A character is one symbol: a letter, a digit, a space or a punctuation mark. Python has no separate character type. A single character is just a str of length 1, so "a" and "abc" have the same type.

Think of a row of numbered lockers. Each locker holds one character, and the lockers are numbered from 0. The number on a locker is its index. The first locker is index 0, not index 1. The last locker of a string of length 12 has index 11, because 12 lockers are numbered 0 through 11. Python also lets you count from the end: index -1 is the last locker, -2 is the one before it, and so on.

A string is immutable. Immutable means "cannot be changed after it is made". Picture a printed ticket. If you need a different ticket, the printer makes a new one; nobody erases the letters on the old one. A Python string behaves the same way. Once the object exists, its characters never change. Every method that seems to change a string builds and returns a new string, and the old one stays exactly as it was.

These two ideas, numbered positions and immutability, explain almost everything in this lesson.

Creating strings

There are three common ways to make a string.

a = "hello"                  # a string literal in double quotes
b = 'hello'                  # single quotes make the same kind of object
c = "hel" + "lo"             # + joins two strings and builds a new one

All three hold the same characters, and they are equal. Use whichever quote style avoids escaping inside the text. Use "it's" rather than 'it\'s'.

An empty string has length 0 and no characters. It is written "", two double quotes with nothing between them. Do not confuse it with None, which means "no value at all". An empty string is a real string that holds nothing. None is the absence of a string. The difference matters because calling a method on None raises an error, while len("") returns 0.

Worked example 1: a tour of everyday methods

The first program calls the methods you will use most. Read it line by line and predict each result before you run it. Then compare your prediction with the output below.

Python · editable

The program takes no input. The output is:

12
H
World
Hello
4
8
8
True
True
False
HeLLo, WorLd
padded|
ababab
True True False
Hello, World

Here is what each line does.

  • len(s) counts the characters. H, e, l, l, o, ,, the space, W, o, r, l, d make 12. len is a function, not a method, so you write len(s) and not s.len().
  • s[0] returns the character at index 0, which is H. Python has no separate character type, so the result is a string of length 1.
  • s[7:] returns everything from index 7 to the end. Index 7 is W, so the answer is World.
  • s[0:5] returns the characters from index 0 up to but not including index 5. That is indexes 0, 1, 2, 3 and 4, which spell Hello. The end index is exclusive: it marks the first position you do not want. This slice syntax is called slicing.
  • s.find("o") returns the index of the first o, which is 4. s.find("o", 5) starts searching at index 5, so it skips the first o and finds the one at index 8.
  • s.rfind("o") searches from the right and finds the last o, which is again index 8.
  • "World" in s tests for a piece of text. It returns True or False. startswith and endswith do the same check at one end.
  • s.replace("l", "L") returns a new string with every l changed to L. The original s is untouched.
  • strip() removes whitespace from both ends. It removes spaces, tabs and line breaks.
  • "ab" * 3 joins three copies of ab.
  • "".isspace() is False, because isspace() needs at least one character and all of them must be whitespace. " ".isspace() is True.

When you read a method's documentation, look for three things: what it returns, whether the end index is included, and what happens when the input is out of range. In Python, find returns -1 when nothing matches, while indexing and in behave differently. Those three facts answer most of the questions you will meet.

Worked example 2: indexes and slice ranges

Positions are the source of most string bugs, so it is worth drawing them. Write the string on a line and put each index under its character:

index:   0 1 2 3 4 5 6 7 8 9 10 11
char:    H e l l o , _ W o r  l  d

The slice s[0:5] takes the characters under indexes 0 to 4, which is Hello. The slice s[7:] takes the characters under indexes 7 to 11, which is World. The character at index 5 is the comma, and it belongs to neither range.

Use this rule every time: the number of characters in s[a:b] is b - a, as long as both numbers are inside the string. For s[0:5] that is 5 - 0 = 5 characters. For s[7:12] it would be 12 - 7 = 5 characters, which is World. The end index may equal the length, but it may not be bigger for a plain index. Slices are more forgiving, as the next section shows.

Now consider the last character. Its index is len(s) - 1, and Python also lets you write s[-1]. A loop that visits every character runs from 0 up to, but not including, len(s):

for i in range(len(s)):
    print(s[i])

range(len(s)) produces the numbers 0, 1, ..., up to len(s) - 1. If you ask for range(len(s) + 1), the last pass asks for index 12 in a string of length 12. That raises an IndexError, and you will see that message later in this lesson.

For most loops over text, you do not need an index at all. The next form is the Python way:

for ch in s:
    print(ch)

Use the index form only when you need the position.

Tracing a slice by hand

Trace "banana"[2:5] step by step to see the rule working.

  1. The string is b a n a n a, with indexes 0 to 5.
  2. The start index is 2, which is the first n.
  3. The end index is 5, which is excluded.
  4. Collect indexes 2, 3 and 4: n, a, n.
  5. The result is "nan".

Do the same trace for "banana"[3:]. You start at index 3 and run to the end: a, n, a. The result is "ana". Tracing on paper is slow at first, but it is the fastest way to find an off-by-one bug. When your code is wrong, write the indexes above the characters and walk through the calls.

Comparing strings: == versus is

This is the most important rule in the lesson. The operator == asks whether two strings hold the same characters. The operator is asks whether two names point to the very same object in memory. These are different questions.

Python · editable

The output is:

True
True
True
False
True
True
True
97 98
3

Read the first two lines carefully. a == b is True, and so is a is b. Here CPython, the standard Python program, reuses one object for both literals. The a is b result depends on that detail, and the next section explains why you should not rely on it. c is built at run time by "".join, so it is a new object. a == c is True because the characters match, and a is c is False because the two names point at different objects.

For text comparison, always use ==. Use .lower() or .casefold() on both sides when letter case should not matter. Do not use is for text. The word is is for checking against None, as in if name is None. It also breaks for text read from a user, a file or a network, because those strings are new objects.

Python also compares strings in order. "apple" < "banana" is True because a comes before b. Comparison goes character by character. At the first difference, the character with the smaller code wins. The code of a is 97 and the code of b is 98. When one string is a prefix of the other, the shorter one comes first: "pea" < "pear" is True. Python returns True or False for < and >. It does not return the numbers -1, 0 and 1 that some languages use. The function sorted uses these same comparisons, so a list of words sorts in dictionary order without extra code.

What happens inside: interning and identity

Why does a is b print True when b was written separately? CPython keeps a shared table of some strings, and when a short string literal appears in one place, the compiler can reuse an object that already exists. This is called interning. Interning is an optimization, and the rules for which strings are shared are not part of the language. Two strings can be equal and still be different objects, and two equal literals can be the same object or not depending on how the code was compiled.

The picture in words: a and b both point at one shared "hi". The variable c points at a second "hi" that was built at run time. Python's id function shows the address of an object. Two names with the same id point at the same object. Your output will show different numbers, so do not compare them with printed values from this page.

a = "hi"
c = "".join(["h", "i"])
print(id(a) == id(c))

This prints False on CPython. The id values themselves change from run to run and from machine to machine, so only the comparison is worth reading.

Two lessons follow. First, is compares identities, not contents, so it reports False for a and c even though they hold the same letters. Second, you cannot rely on is returning True for literals either, because interning is an implementation detail. Use == and never think about interning when you write business logic.

Immutability is what makes sharing safe. If nothing can ever change the characters, every name that points at the object sees the same, correct text forever.

Immutability in practice

Immutability is a promise: the contents of a string object never change after it is created. Here is the program that proves it.

Python · editable

The output is:

inside: ADA
outside: ada
hello HELLO
one two
TypeError: 'str' object does not support item assignment

Trace the first part. The module creates the string "ada" and stores it in name. It passes the value to shout, which gives the same object the local name text. Inside shout, the line text = text.upper() builds a new string, "ADA", and points text at it. The function then prints ADA. When shout returns, its name text disappears. The object that name points at is still "ada", because nothing ever changed it. The second print proves it.

The second part shows the same idea without a function call. greeting.upper() returns a new string that is stored in loud. The original greeting still holds hello. A program that forgets to store the result of a method like this one is wrong, and Python does not warn you, because the call is legal. This is the most common mistake for beginners, so the next section gives it extra attention.

The third part is the one that surprises people. sentence = sentence + " two" looks like changing the string, but it does not. The + operator builds a new string containing one two, and the name sentence is pointed at it. The old "one" is still around until Python frees it. The name changed; the string did not.

The last part shows the rule directly. Python refuses word[0] = "b" with a TypeError. To get a different word, build a new string, for example "b" + word[1:].

Why strings are immutable

There are three practical reasons, and an interviewer may ask for them.

  • Safety when sharing. If you pass a string to a function, that function cannot change the text under you. Your code can assume the value it stored yesterday is the value it has today.
  • Dictionary keys. A dict finds an entry by computing a hash, a number that stands for the key. If the key could change, its hash would change and the entry could never be found again. Immutable strings can be used as dictionary keys and as members of a set. A list cannot, because lists can change.
  • Interning. Sharing one object among many names is only safe when nobody can edit it.

The cost is that building text piece by piece creates many temporary objects. That is the topic of the next lesson, which introduces "".join and the list-based way to build text in a loop.

Reading text from the user

A program often gets its text from the keyboard or from a file. The built-in function input() reads one line from the keyboard and returns it as a string. It removes the line break at the end, and it keeps any spaces inside the line.

Before you run the next program, type the input shown below. The program is also saved in the module as plain code, because it reads from the keyboard.

line = input()
 
vowels = 0
for ch in line:
    if ch.lower() in "aeiou":
        vowels += 1
print("Vowels:", vowels)

Type this input when the program waits for it:

Programming is fun

The output is:

Vowels: 5

The loop visits every character in the line. For each character it calls ch.lower(), so A and a both count. The expression ch.lower() in "aeiou" is True when the character is one of the five letters. The in test on a string checks whether the one-character string is a piece of "aeiou". The letters that count are o, a and i in Programming (three), i in is (one) and u in fun (one). The total is 5.

Notice that ch.lower() does not change ch. It returns a lowercase copy of one character, and the line itself stays the same.

Two more facts about input(). It always returns a string, even when the user types digits, so int(input()) is needed to get a number. And if the user types nothing and presses Enter, input() returns the empty string, not None.

Worked example 3: cleaning user input

Text from a user is rarely tidy. People type extra spaces, mix capital letters and forget that a name may have one word or two. A common first step is to clean the text, then compare or split it. The next program does that for a name typed at a login prompt.

stored = "ada lovelace"
typed = input()
 
cleaned = typed.strip().lower()
print(f"[{cleaned}] length {len(cleaned)}")
if cleaned == stored:
    print("Welcome back, Ada.")
else:
    print("Name not recognised.")
print("First name:", cleaned[0:cleaned.find(" ")])
print("Initial:", cleaned[0])

Type this input, with the spaces exactly as shown, two at the start and two at the end:

  ADA LOVELACE  

The output is:

[ada lovelace] length 12
Welcome back, Ada.
First name: ada
Initial: a

Trace the first two lines. The typed text is 16 characters long: two spaces, the 11 letters and the word gap, then two spaces. strip() removes the two spaces at each end and returns a new string of 12 characters. lower() is then called on that new string, and the result is stored in cleaned. Because the calls are chained, the second call works on the result of the first, and nothing is thrown away. The == test passes, because both strings hold ada lovelace.

The expression cleaned.find(" ") finds the first space at index 3. The slice cleaned[0:3] returns the characters at indexes 0, 1 and 2, which is ada. The cleaned[0] expression returns a, the first character.

Now type a single word:

bob

The output is:

[bob] length 3
Name not recognised.
First name: bo
Initial: b

No error appears, and the first name is wrong. The cause is the value -1. find(" ") returns -1 when there is no space, and the slice cleaned[0:-1] then means "from index 0 up to but not including the last character". Python reads -1 as a position counted from the end, so it returns bo. This is a silent bug, and it is harder to find than an exception. Python's slices never raise an error for out-of-range ends, so a wrong value can pass straight through. The fix is to check the result of find before you use it:

space = cleaned.find(" ")
first = cleaned if space == -1 else cleaned[:space]

When there is no space, the whole text is the first name. The same care is needed for cleaned[0]. If the user had typed nothing, cleaned would be the empty string, and cleaned[0] would raise IndexError. A good rule is to ask, for each index you compute, which values can make it invalid, and then test those values before the call.

Characters are numbers

Each character has a number called its code point. The built-in ord returns the number for a character, and chr returns the character for a number. The letter a is 97, b is 98, and so on up to z at 122. Capital letters start at 65: A is 65 and Z is 90. You can subtract characters through their numbers and compare them, and Python shows the result as a number.

print(ord("a"))              # 97
print(ord("d") - ord("a"))   # 3
print(chr(ord("a") + 1))     # b
print("a" < "b")             # True

The expression ord("d") - ord("a") gives 3, because 100 - 97 = 3. This trick turns a lowercase letter into a position from 0 to 25. Letter counting uses that position as a list index. The next program counts every letter in a line of text.

text = input()
 
count = [0] * 26
for ch in text:
    if "a" <= ch <= "z":
        count[ord(ch) - ord("a")] += 1
for i in range(26):
    if count[i] > 0:
        print(chr(ord("a") + i), count[i])

Type this input:

banana bandana

The output is:

a 6
b 2
d 1
n 4

Here is the trace for the first few characters. The character b has code 98, so ord("b") - ord("a") is 1, and count[1] goes from 0 to 1. The character a has position 0, so count[0] goes up. The space is not between a and z, so the if skips it. At the end, count[0] holds 6 because banana has three as and bandana has three more. The last loop turns each position back into a letter with chr(ord("a") + i).

The line "a" <= ch <= "z" is a chained comparison. Python reads it as two comparisons joined by and, so you do not need to write it twice. The code [0] * 26 makes a list of 26 zeros. A list is covered in the next lesson.

Python has no char type, so the comparison ch == "a" is valid and means the same as ch == 'a'. The mistake is therefore different from Java's: a longer string compared to a character never matches, and Python does not report an error for it.

Exceptions and reading error messages

When something goes wrong, Python stops your program and prints a traceback. A traceback lists the lines of code that were running when the error happened, and it ends with the exception type and a message. The type tells you what kind of problem happened. The message gives the details. The traceback shows which line of your code caused it.

Three exceptions appear often with strings.

  • IndexError happens when an index is outside the string, for example s[10] on a string of length 5.
  • AttributeError happens when you ask an object for a method or attribute it does not have, for example s.length or a method call on None.
  • TypeError happens when an operation gets a value of the wrong type, for example "ab" + 1, or when you try to change a string in place.

The program below catches each of them so you can see the exact messages.

Python · editable

The output is:

index: string index out of range
slice: 'llo'
none: 'NoneType' object has no attribute 'upper'
int: invalid literal for int() with base 10: '12a'
concat: can only concatenate str (not "int") to str
attr: 'str' object has no attribute 'length'

Read each message as a short report. string index out of range says the index you asked for does not exist. The message does not print the index or the length, so you must read the line of code to find them. The slice s[2:9] does not raise an error at all. It returns 'llo', because a slice stops at the end of the string. That is a difference from indexing that beginners often miss.

The AttributeError for None names the type, NoneType, and the method you tried to call. When you see it, look at the line in the traceback, find the variable on that line and check why it is None.

The ValueError from int names the text that could not be read. int("12a") fails because a is not a digit.

Uncaught exceptions look like this. The next program does not catch the error, so Python prints the traceback and stops. The file is main.py, and the path shown on your machine will differ.

word = "Hello"
print(word[10])

The real output on Python 3.14 is this:

Traceback (most recent call last):
  File "/home/example/main.py", line 2, in <module>
    print(word[10])
          ~~~~^^^^
IndexError: string index out of range

The last line is the important one. It names the exception and its message. The line that starts with File gives the file and the line number. The caret lines under the code show which part of the line failed. Start reading at the bottom and work up to your own code.

Common mistakes

These are the mistakes that appear most often in beginner code. Each one has a fix you can apply in a few seconds once you recognise it.

Mistake 1: using is for text. The program below looks right, but it can print the wrong answer.

typed = "".join(["y", "e", "s"])
if typed is "yes":
    print("matched")
else:
    print("not matched")

It prints not matched, because typed is a new object built at run time. Python also warns about this pattern with a SyntaxWarning when the literal is written directly. The fix is typed == "yes".

Mistake 2: forgetting that a method returns a new string. This program prints the original text, not the upper-case text:

name = "ada"
name.upper()
print(name)      # prints ada

The fix is name = name.upper(), which stores the new string in the name.

Mistake 3: calling len as a method, or using a field name. The code s.len() raises an AttributeError, and so does s.length. Python shows these as runtime errors, not compile errors, because it checks them only when the line runs:

AttributeError: 'str' object has no attribute 'len'

The fix is len(s). The built-in len works on strings, lists and most other containers.

Mistake 4: a slice that silently cuts text short. "abc"[1:4] returns "bc", not an error, because the slice stops at the end of the string. If you expected three characters, the result is wrong without any warning. Check the length after slicing when the input may be short. A plain index such as "abc"[4] does raise IndexError.

Mistake 5: a None value. t = None followed by t.upper() raises AttributeError: 'NoneType' object has no attribute 'upper'. Check for None before you use a value that might be missing, or make sure the value is always set.

Mistake 6: mixing numbers and strings. "Total: " + 5 raises TypeError: can only concatenate str (not "int") to str. The fix is "Total: " + str(5) or an f-string, f"Total: {5}".

Mistake 7: thinking an empty string is None. "" has length 0 and is a real string. None has no string at all. A check such as if not s: is true for both, which is useful when you only need a value to be present, but it hides the difference, so use s is None or not s on purpose.

Debugging walkthrough: a method that changes nothing

Here is a small function with a bug. It is meant to clean a user's typed name by removing leading and trailing spaces and making it lowercase, then check whether it is "ada".

def is_ada(raw):
    raw.strip()
    raw.lower()
    return raw is "ada"
 
print(is_ada("  ADA  "))

The program prints False. A beginner usually expects True. Debug it the way a professional does: write down what each line should change, then check the actual value.

  1. Start with the input, " ADA ". The length is 7, including the two spaces at each end.
  2. Line one, raw.strip(), should produce "ADA". Does it? No. strip() returns a new string, and the result is thrown away. raw still holds " ADA ".
  3. Line two, raw.lower(), has the same problem. Its result is also thrown away.
  4. Line three compares raw with "ada" using is. Even if raw held "ada", is compares objects, so it is not a safe test here either.

There are two bugs: the results of the method calls are never stored, and is is used for text. The corrected version is:

def is_ada(raw):
    cleaned = raw.strip().lower()
    return cleaned == "ada"
 
print(is_ada("  ADA  "))

This prints True. The method calls are chained, so each result feeds the next call, and the final value is stored in cleaned. The comparison uses ==. The function is also shorter, which usually means it is easier to check.

A habit that catches this bug early is to print a value after each step while you are debugging. A line such as print(f"[{cleaned}]") shows the text with brackets, so spaces at the ends are visible. Brackets are the simplest way to see a hidden space. Python's repr function does the same job with quotes and escapes: print(repr(" ADA ")) prints ' ADA '.

Variations and edge cases

Good string code works for the inputs you did not think about at first. Check these cases before you trust a function.

  • The empty string. len("") is 0. ""[0] raises IndexError, because there is no index 0. A loop with for ch in s: never runs for an empty string, which is what you want.
  • Only spaces. " ".isspace() is True. " " == "" is False, because the string holds three characters. Choose the test that matches your rule, and use .strip() == "" when you want "blank counts as empty".
  • Case. "Level" == "level" is False. "Level".casefold() == "level".casefold() is True. Use casefold() when you compare text that may hold letters from other languages, and lower() for everyday English.
  • Whitespace. strip() removes spaces, tabs and line breaks at both ends. It also removes some characters that look like spaces but are not plain spaces, such as the non-breaking space. Use strip() by default.
  • Missing text. A function that returns None for a missing value forces every caller to check. Decide early whether a value can be None, and check it once at the boundary of your code.
  • Ranges that touch the end. s[len(s):] is the empty string, and it is allowed. s[len(s) + 1:] is also the empty string, because slices are forgiving. A plain index at len(s) raises IndexError.
  • Searching for a missing piece. find returns -1 when the piece is not found. Test for -1 before you use the result as an index, or the next slice can return the wrong text, as the bob example showed.

Each of these is a single line of code to check, and each one is a test case you should write for every string function you build.

Characters beyond plain English

So far every character has been a letter, a digit or a common punctuation mark. Python 3 handles every Unicode character directly. A string is a sequence of code points, one per character, and len counts code points. The emoji below is one code point, so its length is 1.

smile = "😀"
print(len(smile))
print(ord(smile))
print(hex(ord(smile)))
print(smile.encode("utf-8"))
print(len("é"), len("é"))

The output is:

1
128512
0x1f600
b'\xf0\x9f\x98\x80'
1 2

The last line shows that "é" as one code point has length 1, while "e" followed by a combining accent, written "é", has length 2. Both look the same on the screen. The encode method turns the text into bytes. UTF-8 is the usual byte format for files and web pages, and the emoji takes four bytes in it.

Other languages count differently. In Java, length() counts 16-bit units, so one emoji has length 2. Python counts code points, so the same emoji has length 1. Neither count is the number of symbols you see on the screen when a character is built from several code points, such as the accented e above. For full text handling of user-perceived characters, the standard library's unicodedata module and third-party libraries help, but the lessons in this module treat one code point as one character. Say that assumption out loud in an interview: "I assume each character is one code point, which holds for plain English text." That one sentence shows that you know where the simple model stops.

Interview view: what interviewers ask first

Interviewers do not usually ask you to define immutability. They ask questions that depend on it, and the answers reveal whether you understand the cost of each operation. The table below lists the costs you should be able to state for CPython.

OperationWhat it costsWhy
len(s)O(1)The length is stored in the object
s[i]O(1)Direct access to one position
s == tO(n) in the worst caseCompares characters one by one until a difference
s[a:b]O(b - a)Copies the characters into a new string
x in s and s.find(t)O(n * m) in the worst caseTries each start position against a pattern of length m
s + tO(len(s) + len(t))Builds a new string with a copy of both parts

The cost of slicing surprises people, because it looks like a simple lookup. It is not. A slice copies the characters you ask for. A loop that slices a long string to read one character at a time does far more work than you expect. Read the characters with an index or with for ch in s instead.

A typical interview warm-up is this question: "Is one string a rearrangement of another?" The answer, which you will meet in the palindromes and anagrams lesson, uses a count of each letter. Before you write any code, say what you will compare, what it costs and what the edge cases are. That habit, more than any single algorithm, is what interviewers listen for.

Practice

Try each exercise before you open the solution. The first few are short; the last ones combine several ideas from this lesson.

  1. Print the last character of any string s without writing the index by hand. (Hint: negative indexes count from the end.)
  2. What does "banana".find("an") return, and what does "banana".rfind("an") return? Predict both, then check.
  3. Count how many times the letter a appears in "banana" using a loop. Then count how many times the piece "an" appears, using find in a loop, moving the start position forward after each match. Which result is 2 and why?
  4. Write a function is_blank_or_none(s) that returns True when s is None, empty, or contains only whitespace.
  5. Given word = "python", write one line that prints PYTHON without changing word. Then write one line that changes word to PYTHON.
  6. Explain in two sentences why s = "abc" followed by s + "def" and then print(s) still prints abc, and which line changes s.
  7. Write a function middle(s) that returns the middle character of an odd-length string, and the two middle characters of an even-length string. Test it on "abcde", "abcd" and "". For the empty string, decide what you want and document it.
  8. Read one line with input(). Print YES if it contains the word java in any letter case, and NO otherwise. Lowercase the line once, then use in.
  9. Write a program that reads a line and prints the position of each e, one per line, using only indexes and a loop.
  10. A colleague writes if name.strip() is "": to check for a blank name. Give two separate reasons this is wrong, and write the correct condition.

Show the answer

  1. print(s[-1]) prints the last character. Negative indexes count from the end, so -1 is the last position. s[len(s) - 1] gives the same result.
  2. "banana".find("an") returns 1, the first position where an begins. "banana".rfind("an") returns 3, the last position. The two an pieces start at 1 and 3.
  3. Using a loop over characters, the count of a is 3. Using find in a loop, the piece an appears at positions 1 and 3, so the count is 2. Each time find returns a position, the next search starts one character after it. The first count counts letters, the second counts pieces of two letters.
  4. def is_blank_or_none(s): return s is None or s.strip() == "". The strip() call removes all whitespace, so an empty string and a string of only spaces both become "". The explicit check for None is needed because None has no strip method.
  5. print(word.upper()) prints PYTHON and leaves word alone. word = word.upper() changes the name to point at the new upper-case string.
  6. s + "def" returns a new string, "abcdef", and the program throws it away. The name s still points at "abc", because strings are immutable. To keep the result, write s = s + "def".
  7. Let n = len(s). If n is 0, return "", or raise an exception if you prefer, and say so in a comment. If n is odd, return s[n // 2]. If n is even, return s[n // 2 - 1 : n // 2 + 1]. For "abcde" the answer is c, for "abcd" it is bc.
  8. text = input().lower(), then print("YES" if "java" in text else "NO"). Lowercasing once makes the test case-blind.
  9. Loop over range(len(line)); if line[i] == "e", print i. For "where are the eels" this prints 2, 4, 8, 12, 14 and 15, one per line.
  10. Two reasons. First, is compares object identity, so the test can fail even when the stripped text is empty. Second, the code "" in an is test is a literal, and interning makes the result depend on how the code was compiled. The correct test is name.strip() == "", or not name.strip() when name is never None.

What you now know

  • A string is a row of characters with indexes from 0 to len(s) - 1. Negative indexes count from the end.
  • The end index of s[start:end] is exclusive, and the result has end - start characters. Slices never raise an error for out-of-range ends.
  • Compare text with ==, never is. is compares object identity.
  • A string is immutable. Every method returns a new string, so you must store the result.
  • Interning can make is seem to work for literals. Do not depend on it.
  • input() reads a whole line as a string, including spaces, and removes the line break.
  • A character is a string of length 1, and ord and chr convert between characters and code points. ord("d") - ord("a") gives a letter's position from 0 to 25.
  • The error messages for index and None problems name the type of the problem. Read the line number in the traceback, then check the values on that line.

The next lesson covers building text in a loop with "".join and the list-based approach, and the cost of building text by repeated +.

Practice this topic: coding problems for Strings →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.