Strings and Text
Building and shaping text: f-strings, the string methods worth memorising, why a string can never be changed in place, and what happens when text is not plain English.
Building and shaping text: f-strings, the string methods worth memorising, why a string can never be changed in place, and what happens when text is not plain English.
A customer database ended up with two records for the same person. Same name, same email, same everything on screen. The only difference was a single space at the end of one of them, typed by someone who pressed the spacebar before Enter and never knew.
Nearly everything a program touches arrives as text, and text
arrives dirty. By the end of this lesson you will have a much
better way to build messages than the + and str() from the
last lesson, know the handful of string operations that come up
constantly, and understand why a string can never be changed
once it exists.
A string is a piece of text. You write one by putting it in quotes — single or double, as long as you match:
photographer = "Ana Duarte"
location = 'Lisbon'There is no difference between the two, which is useful when the text contains a quote:
caption = "Ana's first exhibition" # apostrophe inside doubles
quote = 'She said "yes" immediately' # doubles inside singlesFor text spanning several lines, use three quotes:
description = """Taken at dawn on the Praça do Comércio,
before the tram queues formed."""Whatever you type inside triple quotes is kept exactly, including the line breaks.
Here is the message from the last lesson, built by joining:
photo_count = 400
photographer = "Ana Duarte"
message = photographer + " uploaded " + str(photo_count) + " photos"It works. It is also hard to read, easy to get wrong — one
missing space and you get Ana Duarteuploaded — and it forces
you to convert every number by hand.
An f-string does the same job by letting you write the
sentence and drop the values into it. Put an f before the
opening quote, then put names in curly braces:
message = f"{photographer} uploaded {photo_count} photos"
print(message) # Ana Duarte uploaded 400 photosThe sentence is now readable as a sentence.
Three things to get right per value.
You supply the spaces by hand, so one missing quote gives you
Ana Duarteuploaded.
Every number needs a str() around it or the whole line
raises a TypeError.
Write the sentence, drop the values in.
Spaces are where you typed them, because you typed the sentence.
Numbers convert themselves, and anything can go in the braces — not just a name.
print(f"{photo_count} photos, about {photo_count / 24:.1f} per hour")
# 400 photos, about 16.7 per hourThat :.1f is a format specifier — instructions for how to
display the value, after a colon. A few are worth knowing:
size = 1234.5678
print(f"{size:.2f}") # 1234.57 two decimal places
print(f"{size:,.2f}") # 1,234.57 with thousands separators
print(f"{42:>8}") # " 42" right-aligned in 8 columns
print(f"{42:08}") # 00000042 padded with zerosThe alignment ones are how you produce output in tidy columns without counting spaces by hand.
A string is a sequence of characters, and you can get at them by position. Positions start at zero:
name = "Lisbon"
print(name[0]) # L
print(name[3]) # b
print(name[-1]) # n negative counts from the end
print(name[-2]) # oStarting at zero feels arbitrary for about a week and then
becomes invisible. The negative indexes are a genuine
convenience: [-1] is the last character without needing to
know the length.
Taking a range of characters is called slicing:
print(name[0:3]) # Lis from 0, up to but NOT including 3
print(name[:3]) # Lis from the start
print(name[3:]) # bon to the end
print(name[:]) # LisbonThe "up to but not including" rule is the one to internalise.
name[0:3] gives three characters, positions 0, 1 and 2. It
reads oddly at first and has a payoff: name[:3] and name[3:]
split the string cleanly with no overlap and no gap, because the
same number ends one and starts the other.
print(len(name)) # 6 how many charactersStrings come with methods — behaviour you call by writing a dot and the name after the string.
messy = " Ana Duarte\n"
print(messy.strip()) # "Ana Duarte" whitespace off both ends
print(messy.strip().upper()) # "ANA DUARTE"
print("Lisbon".lower()) # "lisbon"strip() earns its place early. Text from files and from users
arrives with trailing spaces and invisible line-break characters
constantly, and two strings that look identical on screen will
compare as different if one has a stray space.
Splitting and joining are the pair you will reach for most:
row = "Ana Duarte,Lisbon,400"
fields = row.split(",") # ['Ana Duarte', 'Lisbon', '400']
print(fields[1]) # Lisbon
print(" | ".join(fields)) # Ana Duarte | Lisbon | 400split cuts a string into a list at every occurrence of what you
give it. join is its reverse, and reads backwards the first
time: you call it on the separator, and hand it the pieces.
Almost every piece of text-wrangling you ever write is those two with something in between.
split
One string becomes a list of pieces.
"Ana,Lisbon,400".split(",")
Do the work
Strip whitespace, fix capitalisation, drop or reorder fields, convert the number.
join
The pieces become one string again, in whatever shape you
need. " | ".join(fields)
Searching and replacing:
title = "sunrise_over_lisbon.jpg"
print(title.replace("_", " ")) # sunrise over lisbon.jpg
print(title.endswith(".jpg")) # True
print(title.startswith("sunrise")) # True
print("lisbon" in title) # True
print(title.find("over")) # 8 position, or -1in is the one to prefer when you want a yes or no. find
gives a position, and returns -1 when there is no match —
which is a real trap, because -1 is a valid position meaning
"last character", so a careless if title.find("x") treats "not
found" as a truthy answer.
Strings are immutable: once a string exists, nothing can alter it. Every operation that looks like a change actually produces a new string.
name = "lisbon"
name.upper()
print(name) # lisbon - unchanged!This catches everyone once. upper() did its job perfectly and
handed back "LISBON", and nobody kept it. Methods on strings
return the result; they never modify the original.
Bad — calling the method and throwing the answer away.
name = input("Name? ")
name.strip()
name.title()
save_photographer(name) # still " ana duarte\n"Good — keeping what comes back.
name = input("Name? ")
name = name.strip().title()
save_photographer(name) # "Ana Duarte"Nothing errors in the first version. The methods run, produce correct results, and drop them on the floor — so the record is saved with its leading spaces and its line break, and it will never match the same name typed cleanly. You find out weeks later, from a duplicate that is not a duplicate.
One more thing, briefly, because it explains a whole family of errors.
Python strings hold characters, not letters of the English
alphabet. "Praça", "日本" and "🇵🇹" are all ordinary
strings, and len() counts characters rather than bytes.
When text is written to a file or sent over a network it must become bytes, and the rule for turning characters into bytes is called an encoding. The one to use is UTF-8, which handles every character there is.
You will meet this properly in the files lesson. It appears here
so that when you eventually see UnicodeDecodeError, you
recognise it as "something read bytes using the wrong rule"
rather than as a mysterious failure of your text.
WRITING TEXT
"double" 'single' identical; pick to avoid escaping
"""three quotes""" spans lines, keeps them exactly
\" \n \t escaped quote, line break, tab
F-STRINGS
f"{name} has {n} photos" drop values into a sentence
f"{value:.2f}" two decimal places
f"{value:,.2f}" thousands separators
f"{n:>8}" f"{n:08}" right-align, zero-pad
f"{name=}" prints name=value, for debugging
POSITIONS
s[0] s[-1] first, last
s[0:3] from 0, up to but NOT including 3
s[:3] s[3:] to there, from there - a clean split
len(s) how many characters
METHODS (all return a NEW string)
s.strip() whitespace off both ends
s.upper() s.lower() s.title()
s.split(",") cut into a list
", ".join(parts) called on the SEPARATOR
s.replace(a, b) every occurrence
s.startswith(x) s.endswith(x)
x in s yes/no - prefer this
s.find(x) position, or -1 if absent
THE RULE THAT CATCHES EVERYONE
strings are immutable s.strip() changes nothing
keep the result s = s.strip()You can now build readable messages with f-strings, cut text apart and put it back together, clean up what users and files hand you, and you know why a method call that looks like a change is not one. That immutability rule is the same one that will explain the behaviour of tuples later, so it is worth more than the one lesson it appeared in.
Next is Numbers and Arithmetic That Surprises You, which
takes the other type you use constantly and explains why
0.1 + 0.2 does not equal 0.3 — not a bug, not a Python
quirk, and something worth understanding before you ever write
code that handles money.
Before you move on, take a line of text with a real shape to it — a filename, a CSV row, a full name — and pull it apart into its pieces, then rebuild it in a different format. Do it at the interactive prompt so you see each step. Splitting, stripping and rejoining is the most common thing you will ever do to text, and ten minutes of it now makes the rest automatic.