Working With Text
A string is a sequence of characters with a deep toolkit: index it, slice it, clean it, split it, and build new text, none of it changing the original.
Builds on
What you will learn
- -Index and slice a string to pull single characters and substrings out of it
- -Explain why string methods return a new string and leave the original untouched
- -Clean messy text with .strip(), .lower(), and .replace(), and test it with .startswith() and .endswith()
- -Convert between a string and a list using .split() and .join(), and build text with f-strings
The last lesson built a lookup out of a dictionary, and almost every key in it was a string: contacts["ada"], prices["latte"]. A dictionary is glad to fetch those, but it treats each key as one solid, unbreakable token. It never looks inside "ada" to see the three letters. This lesson pries the string open, because most of the data a program touches, names, emails, file paths, lines pulled off a web page, arrives as text and has to be cleaned and reshaped before anything else can use it.
Start with a fact carried over from the dictionary lesson: a string is text, written inside quotes. What that lesson skipped is that a string is not one lump. It is a sequence, an ordered run of characters, first to last, each one sitting at a numbered position. That single idea, a string is a sequence, is the door to everything that follows.
A string is a sequence you can index and slice
Because a string is ordered, every character has an index, a whole number marking its spot, counting from zero. The first character is at index 0, the second at 1, and so on. Reading one character out is called indexing, and it uses square brackets with the position inside.
word = "python"
print(word[0])
print(word[2])
Counting from zero trips up nearly everyone at first. The character at index 0 is the first one, so word[2] is the third character, not the second. Python also counts backward with negative numbers, where -1 is the last character, -2 the one before it, which saves the trouble of measuring the length just to reach the end.
word = "python"
print(word[-1])
Pulling out a whole run of characters at once is called slicing, and it uses two numbers separated by a colon: a start and a stop. The rule that catches people is that the slice includes the start position and stops just before the stop position. So word[1:4] gives the characters at indices 1, 2, and 3, and leaves index 4 out.
word = "python"
print(word[1:4])
That "up to but not including the stop" behavior is not Python being difficult. It means the slice length is simply stop minus start, so word[1:4] is three characters, and two slices that meet at the same number, word[0:3] and word[3:6], join back into the whole string with no gap and no overlap. Leaving a number off means "from the start" or "to the end": word[:3] is the first three characters, word[3:] is everything from index 3 onward.
Indexing and slicing part ways at the edges, and the difference is a real failure mode. Asking for word[99] on a six-letter string raises an IndexError, because there is no character at that position, so a bad index crashes the program. Slicing past the end does not: word[2:99] quietly returns whatever characters actually exist and stops there. A wrong index blows up loudly, a too-long slice just hands back less than expected, which is the quieter and sometimes sneakier of the two.
Given word = "hello", what does word[1:4] give back?
Strings do not change, methods return new ones
Here is the fact that causes more early string bugs than any other: a string is immutable, meaning once it exists it can never be altered in place. Every operation that looks like it edits a string actually builds a brand new string and hands that back, leaving the original exactly as it was.
A method is a function attached to a value, called with a dot: value.method(). Strings come with a deep set of them. .upper() returns an all-caps copy, .lower() returns an all-lowercase copy. The word "copy" there is the whole point. Watch what happens to the original.
greeting = "Hello"
loud = greeting.upper()
print(greeting)
print(loud)
The original greeting still holds "Hello" after .upper() ran. The uppercased text went into a new string, and that new string is what got stored in loud. Nothing about greeting moved. If a program wants greeting itself to become uppercase, it has to reassign the name: greeting = greeting.upper(), which stores the returned copy back under the old name.
Immutability is not a limitation to code around. It is exactly what made those dictionary keys trustworthy in the last lesson. A key that could quietly change out from under the dictionary would wreck the lookup, since the value would no longer sit where it was filed. Python allows only unchangeable values as keys, and a string qualifies precisely because no method can ever alter it in place.
The core cleaning methods all follow this return-a-new-string pattern. .strip() returns a copy with leading and trailing whitespace removed, which matters constantly because text typed by a person or read from a file often carries stray spaces or a trailing newline. .replace(old, new) returns a copy with every occurrence of one substring swapped for another. .startswith(prefix) and .endswith(suffix) are different: they return a boolean, True or False, answering a yes-or-no question about the text without changing it.
raw = " Report_Final.TXT "
clean = raw.strip()
print(clean)
print(clean.endswith(".TXT"))
print(clean.replace(".TXT", ".txt"))
print(clean)
Look at the last two lines together. .replace(".TXT", ".txt") produced a lowercased-extension copy on line three, but line four proves clean never budged. The replacement lives only in the value that print received and then threw away. To keep it, a program has to catch the return value in a name.
The most common string bug for beginners is calling a method and expecting the variable to change on its own. name.strip() on a line by itself does compute a stripped copy, but it drops that copy on the floor, so name still holds the spaced-out original. The fix is to store what came back: name = name.strip(). If a cleaning step seems to do nothing, this is almost always why.
A program runs name = " Robin ", then name.strip() on the next line, then print(name). What prints?
Split and join: the inverse pair
Two methods turn out to be the workhorses of real text handling, and they are opposites of each other. .split(sep) takes one string and chops it into a list of smaller strings, cutting at every place the separator sep appears. The separator itself is thrown away, and what comes back is a list, the ordered collection from earlier in this module.
line = "Ada,Lovelace,1815,London"
fields = line.split(",")
print(fields)
print(fields[0])
print(len(fields))
That is the shape of a CSV file, comma-separated values, where each line is one record and commas mark the boundaries between fields. One .split(",") turns a flat line of text into a list a program can index into and loop over. Called with no argument, .split() splits on runs of whitespace instead, which is the quick way to break a sentence into words.
Going the other direction is .join(), and its shape surprises almost everyone. The separator string is the one that carries the method, and the list of pieces is the argument: separator.join(list_of_strings). It reads backward from how people expect, but it lets any separator glue a list back into one string.
fields = ["Ada", "Lovelace", "1815", "London"]
print(",".join(fields))
print(" ".join(fields))
Split and join are exact inverses when the separator matches: split a string on a comma to get a list, join that list on a comma to get the original string back. One warning worth stating plainly: .join() only works when every item in the list is already a string. Hand it a list with a number inside and Python raises a TypeError, because it will not guess how a number should be spelled out as text.
How do .split() and .join() relate to each other?
Building text with f-strings
Reading and cutting text is half the job. The other half is composing it: dropping a name, a score, or a computed number into a sentence. The clean way to do that is an f-string, a string with the letter f written just before the opening quote, which lets any value slot into a pair of curly braces inside the text.
name = "Robin"
score = 42
print(f"Hi {name}, you scored {score}")
Whatever sits inside the braces is worked out and turned into text right there in place. That includes plain expressions, not only bare names, so f"Total: {price * quantity}" computes the product and drops the result in. An f-string also sidesteps the type trap from earlier in this module: score is a number, but the f-string converts it to text automatically as it builds the sentence, so there is no need to convert it by hand the way string concatenation with a plus sign would demand.
Now put the pieces together on a real chore: cleaning messy input so it can be used as a stable key. Text that arrives from a form or a keypad is unreliable. It might have stray spaces, mixed capitalization, or a swapped separator, and two values that a person would call identical, " Ada@Example.COM " and "ada@example.com", are two different strings to Python and would land in two different dictionary slots. A short clean-up fixes that.
def normalize_email(raw):
return raw.strip().lower()
print(normalize_email(" Ada@Example.COM "))
print(normalize_email("ADA@example.com"))
Saving that as clean.py and running it on a computer with Python installed gives one consistent result for both inputs.
$python3 clean.pyThe chain raw.strip().lower() reads left to right and works because each method hands back a string that the next method can act on. .strip() returns the space-free copy, and .lower() is called on that copy, not on the original. Chaining like this is common and safe precisely because strings are immutable: every step produces a fresh string, so nothing earlier in the chain gets stepped on. Two records that used to disagree now normalize to one key, and a dictionary built on that key will match them.
Write a script that starts with the line raw = " Name: Ada Lovelace ". First store raw.strip() in a new variable and print it, and confirm the leading and trailing spaces are gone. Then call .split(":") on the stripped value to break it into a label and a value, and print the resulting list so you can see the two pieces.
Take the second piece from that list, strip it too, and store it as person. Finally, print a sentence with an f-string: f"Contact stored: {person}". Predict each line of output before running it. If the final line reads Contact stored: Ada Lovelace with no stray spaces, every method in the chain returned what you expected and you stored each result instead of dropping it.
A string toolkit, indexing and slicing to read text apart, immutable methods that always return a fresh copy, split and join to move between text and lists, and f-strings to build new text, covers most of what programs do with words. All of it so far has happened in memory and vanishes the instant the program stops. The next lesson, reading-and-writing-files, takes this same cleaned and reshaped text and makes it last: pulling lines out of a file on disk, which arrive as exactly the kind of messy strings this lesson learned to tame, and writing new text back so it survives after the program ends.