In the previous lesson you discovered that a list is a sequence: ordered elements accessed by index. What you may not have seen coming is that you have been using another sequence since module 2 without calling it that. Text —the str type— is exactly that: a sequence of characters. Everything you learned about indices, slices and for traversals works the same way on a string, with one decisive difference: a string is immutable.
And this is not a theoretical ornament. In EasyTask almost everything is text: the titles Marta types, the names of the team, the priorities, the lines we will later save to a file. Handling strings well is what separates a program that accepts " BOOK fair " and "book Fair" as two different tasks from one that understands they are the same.
Contents
- The string as an immutable sequence
- Indices, slices and traversal
- Cleaning:
strip,lstrip,rstrip - Upper and lower case
- Searching inside the text
- Checking the content
- Transformation and padding
- Splitting and joining: the bridge to lists
- Multiline, escapes and raw strings
- f-strings,
str()and Unicode - Comparing and sorting text
- Application: task titles and codes at Alba Studio
- Common mistakes and tips
- Exercises
- Conclusion
- The string as an immutable sequence
A string is a sequence of characters with a fixed order. len(), indices and the in operator work exactly as they do in a list:
title = "Book fair poster"
print(len(title)) # 16 (spaces count)
print(title[0], title[-1]) # B r
print("fair" in title) # TrueThe difference is here:
A string cannot be modified. Not a single character. Everything that looks like it modifies text actually creates a new string and leaves the original untouched:
title = "book fair poster"
title.upper() # creates "BOOK FAIR POSTER" and throws it away
print(title) # book fair poster -> unchanged
title = title.upper() # now yes: we reassign the name
print(title) # BOOK FAIR POSTER| List | String | |
|---|---|---|
Indices, slices and for |
Yes | Yes |
| Modify an element | Yes | No: TypeError |
| Methods that modify in place | append, sort… |
None |
| Methods that return something new | sorted, copy |
All of them |
Hold on to this idea, because it explains the number one mistake with strings: if you do not store the result, nothing has happened. And in exchange for the restriction there is an advantage: being immutable, strings can be used as dictionary keys, which you will see in 05-03.
- Indices, slices and traversal
Everything from 05-01 applies unchanged, except that here the result of a slice is another string:
client = "Sole Bakery"
print(client[0:4]) # Sole
print(client[-6:]) # Bakery
print(client[:3].upper()) # SOL -> first three letters in upper case
print(client[::-1]) # yrekaB eloS (backwards)To traverse character by character, the direct for; and enumerate if you need the position:
for letter in "Alba":
print(letter, end=" ") # A l b a
for i, letter in enumerate("Alba"):
print(f"{i}:{letter}", end=" ") # 0:A 1:l 2:b 3:aThe counter pattern from 03-02 works the same way here: traverse the text adding 1 every time the letter meets a condition. Almost always, though, there is a method that solves it in one line —counting spaces by hand gives the same as title.count(" ")—, so traverse character by character only when the condition is one of your own.
- Cleaning:
strip, lstrip, rstrip
strip, lstrip, rstripWhen text comes from input() or from a file, it almost always brings rubbish around it: spaces, tabs or the trailing newline. The strip family removes it.
| Method | What it removes | Example |
|---|---|---|
.strip() |
Spaces from both ends | " alba ".strip() → "alba" |
.lstrip() |
Only from the left | " alba".lstrip() → "alba" |
.rstrip() |
Only from the right | "alba\n".rstrip() → "alba" |
.strip(chars) |
Any of those characters | "**alba**".strip("*") → "alba" |
With entry = " Book fair poster \n", f"[{entry.strip()}]" prints [Book fair poster]. Watch out for a frequent misunderstanding: .strip("abc") does not delete the substring "abc", but any a, b or c sitting at the ends. And strip never touches the inside: " a b ".strip() leaves "a b".
- Upper and lower case
| Method | Effect | "book FAIR poster" gives |
|---|---|---|
.upper() |
All upper case | BOOK FAIR POSTER |
.lower() |
All lower case | book fair poster |
.title() |
First letter of each word | Book Fair Poster |
.capitalize() |
First letter of the string, the rest lower case | Book fair poster |
.casefold() |
Like lower, but more aggressive |
book fair poster |
The two typical uses are different and it is best not to mix them. To compare, you normalise to lower case (nobody should have to type "Marta" with the exact capital letter for the program to accept it). To display, you pick the pretty form:
answer = " MARTA "
print(answer.strip().lower() in ("marta", "luis", "nuria")) # True
print(answer.strip().capitalize()) # Marta.casefold() is the rigorous version of .lower() for international comparisons: in German it turns ß into ss, which .lower() does not do. For English, both behave the same; use .casefold() if you compare text that may come in any language.
- Searching inside the text
| Method | Returns | If it finds nothing |
|---|---|---|
substring in text |
True / False |
False |
.find(sub) |
Index of the first occurrence | -1 |
.rfind(sub) |
Index of the last occurrence | -1 |
.index(sub) |
Index of the first occurrence | ValueError |
.count(sub) |
Number of occurrences | 0 |
.startswith(pre) |
True if it starts like that |
False |
.endswith(suf) |
True if it ends like that |
False |
title = "Book fair poster, autumn fair banner"
print("fair" in title) # True -> the most readable if you only want to know
print(title.find("fair")) # 5
print(title.rfind("fair")) # 25
print(title.find("book")) # -1 -> it is case sensitive
print(title.count("fair")) # 2
print(title.startswith("Book")) # True
print(title.endswith((".png", ".pdf"))) # False -> it accepts a tuple of optionsThe difference between .find() and .index() is the one you already know from lists: find returns -1 when there is nothing, whereas index raises ValueError. Use find when "not being there" is a normal case, and remember that -1 is a valid index in Python, so never use the result of find without first checking that it is not -1.
- Checking the content
These methods return True or False and are used to validate before converting, just as you did in 02-04.
| Method | True when the string… |
|---|---|
.isdigit() |
Contains only digits (and is not empty) |
.isalpha() |
Contains only letters |
.isalnum() |
Contains only letters or digits |
.isspace() |
Contains only whitespace |
print("365".isdigit(), "3.5".isdigit(), "-3".isdigit()) # True False False
print("Nuria".isalpha(), "Nuria 1".isalpha()) # True False
print("ALB01".isalnum(), "".isalnum()) # True False
print(" ".isspace()) # TrueNotice the traps: "3.5" is not isdigit (the dot is not a digit), nor is "-3" (the sign is not one either), and an empty string returns False for all of them. That is why if text.isdigit(): before int(text) is still the correct guard for positive integers, but it does not work for decimals or negatives. The robust way of handling those cases is try/except, and it arrives in Debugging and error handling.
- Transformation and padding
| Method | What it does | Example |
|---|---|---|
.replace(old, new) |
Replaces every occurrence | "a-b-c".replace("-", " ") → "a b c" |
.replace(o, n, count) |
Only the first count ones |
"a-b-c".replace("-", " ", 1) → "a b-c" |
.zfill(n) |
Pads with zeros on the left | "7".zfill(3) → "007" |
.center(n, c) |
Centres within n characters |
"ALB".center(9, "-") → "---ALB---" |
.ljust(n, c) |
Aligns to the left | "ALB".ljust(6, ".") → "ALB..." |
.rjust(n, c) |
Aligns to the right | "ALB".rjust(6, ".") → "...ALB" |
zfill is the classic shortcut for numbering: str(7).zfill(2) gives "07", and that way codes come out as 01, 02, 03… instead of 1, 2, 3. The three alignment methods do the same as the :<, :^ and :> specifiers of f-strings; you can use either form, although inside an f-string the specifier looks cleaner.
- Splitting and joining: the bridge to lists
Here is the direct connection with the previous lesson, and it is one of the most useful things in the language.
split(separator) breaks a string apart and returns a list of pieces. With no arguments, it splits on whitespace (grouping consecutive blanks).
line = "Book fair poster;luis;high;3"
print(line.split(";")) # ['Book fair poster', 'luis', 'high', '3'] -> 4 fields
print(" Book fair ".split()) # ['Book', 'fair']
print(line.split(";", 1)) # ['Book fair poster', 'luis;high;3'] -> only the first cut| Method | What it does |
|---|---|
.split(sep) |
Splits on sep from left to right; without sep, on whitespace |
.rsplit(sep, n) |
The same, but counts the n cuts from the right |
.splitlines() |
Splits on line breaks, without keeping the \n |
join does the opposite: it joins the elements of a list into a single string. It is called on the separator, which is what confuses people at first:
team = ["Marta", "Luis", "Nuria"]
print(", ".join(team)) # Marta, Luis, Nuria
print("".join(team)) # MartaLuisNuria
print(", ".join([str(d) for d in [3, 5, 2]])) # 3, 5, 2You read it like this: "take the list and glue it together using this string as the glue". The third line reminds you that join only works with lists of strings: if there are numbers, you have to convert them first, usually with a comprehension from 05-01.
split and join are the pair that turns text into data and data into text. When in 05-05 we read a file of tasks, each line will be turned into fields with split; and to write it, the fields will become a line again with join.
- Multiline, escapes and raw strings
For text spanning several lines there are triple quotes, which respect the line breaks exactly as you type them:
Inside a normal string, some characters are written with an escape sequence, which starts with a backslash:
| Escape | Means |
|---|---|
\n |
Line break |
\t |
Tab |
\\ |
A literal backslash |
\" and \' |
Double or single quote inside the string |
print("Marta\tLuis\tNuria") # separated by tabs
print("Path: C:\\alba\\tasks") # Path: C:\alba\tasks
print('She said "fine" and left') # single quotes outside, double inside
print(r"C:\alba\tasks") # C:\alba\tasks -> raw stringThe last line uses the r prefix of a raw string, which switches escapes off and turns out to be extremely handy with Windows paths. Beware of the classic trap: "C:\tasks" is not what it looks like, because \t is a tab; with r"C:\tasks" there is no surprise.
- f-strings,
str() and Unicode
str() and UnicodeThe f-strings from 02-03 are the normal way of building text with values inside, and their width specifiers are what give EasyTask its aligned tables:
title, assignee, days = "Book fair poster", "Marta", 3
print(f"{title:<24}{assignee:>10}{days:>5}") # fixed width per column
print(f"Average: {10 / 3:.2f}") # two decimals: 3.33str(value) converts any value into its textual representation, and it is essential before concatenating with + or using join. Remember that "Days: " + 3 raises TypeError, whereas "Days: " + str(3) works and f"Days: {3}" works without any explicit conversion.
One idea about Unicode, which is enough for now. Each character of a Python string is a character, not a byte: len("Solé") is 4 even though the é carries an accent, and "é"[0] is "é". But when that text leaves the program —to a file, to the network— it has to be encoded into bytes, and that is where UTF-8 comes in, the standard encoding that knows how to represent accents, ñ, ç and practically any alphabet. If you open a file with the wrong encoding you will see things like Solé. That is why in Saving data to files we will always write encoding="utf-8".
- Comparing and sorting text
Strings are compared with <, > and == character by character, according to the number each character has assigned in the Unicode table. And there a surprise appears:
"Zebra" < "apple" is True because all capital letters come before all lower-case letters: Z is character 90 and a is 97. It is not a whim: it is the order of the table, not the order of a dictionary. The practical consequence is that sorted(["nuria", "Luis", "Marta"]) puts everything starting with a capital letter first. The solution, which you already saw in 04-05 and 05-01, is to sort by a normalised key:
print(sorted(["ana", "Zoe"])) # ['Zoe', 'ana'] -> capitals first
print(sorted(["ana", "Zoe"], key=str.lower)) # ['ana', 'Zoe'] -> expected orderThe same applies to comparing: normalise both sides before comparing. entry.strip().lower() == "marta" accepts " Marta ", "MARTA" and "marta". Accented letters add another step ("Sole" < "Solé"), and handling them correctly requires collation libraries that fall outside this course.
- Application: task titles and codes at Alba Studio
Three text utilities for EasyTask, written with the material of this lesson. The structure of the program does not change yet —a task is still a handful of variables until 05-03—, but these functions will go into the logic section.
def normalise_title(text):
"""Return the title with no leading, trailing or double spaces, initial capitalised."""
clean = " ".join(text.split()) # removes both ends and repeated spaces
return clean.capitalize()
def task_code(client, title, number):
"""Return a code such as ALB-SOL-01 built from the client and the title."""
client_initials = client.strip().replace(" ", "")[:3].upper()
initial = title.strip()[:1].upper()
return f"ALB-{client_initials}-{str(number).zfill(2)}{initial}"
def split_line(line):
"""Split 'title;assignee;priority;days' and return the four fields."""
fields = line.strip().split(";")
if len(fields) != 4:
print("Malformed line:", line)
return None
title, assignee, priority, days = fields
return (normalise_title(title), assignee.strip().capitalize(),
priority.strip().lower(), int(days))Let us try them out:
print(normalise_title(" book FAIR poster ")) # Book fair poster
print(task_code("Sole Bakery", "Seasonal menu", 1)) # ALB-SOL-01S
print(split_line("Book fair poster;luis;high;3")) # ('Book fair poster', 'Luis', 'high', 3)A note on what matters:
" ".join(text.split())is the classic idiom for collapsing spaces:split()with no arguments splits on any block of whitespace and discards the empty pieces;joinputs them back together with exactly one space. It solves the leading and trailing spaces, the double ones and the tabs all at once.client.replace(" ", "")[:3].upper()builds the initials: it removes the spaces, takes three characters with a slice and puts them in upper case. If the client had fewer than three letters, the slice returns whatever is there without an error.str(number).zfill(2)converts to text first (zfillis a string method, not an integer one) and then pads with zeros.split_linereturnsNoneif the number of fields is not the expected one, applying the guard clause from 04-02, and normalises every field with the conventions of the program: capitalised title, capitalised assignee, lower-case priority and days converted to an integer.
That split_line function is, in fact, half of lesson 05-05: reading a file of tasks will consist of applying it to every line.
Common Mistakes and Tips
Forgetting that methods do not modify. title.upper() without assigning does nothing visible; if the text does not change, check that you wrote title = title.upper(). And title[0] = "L" raises TypeError, because the string is immutable: to change one character you have to rebuild, "L" + title[1:].
Using the result of .find() without checking it. If it finds nothing it returns -1, and text[-1] is the last character, so the program will carry on with wrong data instead of failing.
Comparing without normalising. entry == "marta" fails with " Marta ". Apply .strip().lower() to both sides, always at the same point of the program.
Believing that .strip("hello") deletes the word "hello". It deletes any h, e, l or o from the ends. To remove a specific word, .replace("hello", "").
join with numbers. ", ".join([3, 5]) raises TypeError. Convert first: ", ".join([str(d) for d in days]).
Tip: normalise on input, format on output. Store the data already clean (a single canonical form) and decide the presentation when printing. That way you will never have two versions of "Book fair poster" living together in the same agenda.
Exercises
Exercise 1: Predict the output
Say what each line prints and explain why.
text = " Sole Bakery MENU "
print(f"[{text.strip().lower()}]")
print(text.find("Menu"), text.find("MENU"))
print(text.strip().split())
print("-".join(text.split()))
print(text.replace(" ", ""))
print("MENU" < "Menu", "MENU".isalpha())Exercise 2: Text summary card
Write title_summary(title) that takes a title and returns a one-line string with: the normalised title (no extra spaces and capitalised), its number of characters, its number of words and its initials in upper case separated by dots. For " book fair poster artwork " it must return Book fair poster artwork | 24 chars | 4 words | B.F.P.A.
Exercise 3: Line reader for the studio
Given this list of lines, write a program that splits them, discards the malformed ones (those without four fields or whose days are not an integer) and prints an aligned table with title, capitalised assignee, priority in upper case and days. At the end, print the total days of the valid lines.
lines = [
"Book fair poster;luis;high;3",
" sole bakery menu ; MARTA ; medium ; 5 ",
"Vidal logo;nuria;low",
"Summer flyer;marta;high;two",
]Solutions
Solution 1.
strip().lower() cleans and lowers everything. find("Menu") gives -1 because the search is case sensitive and the text says MENU; find("MENU") does find it, at index 14 counting the two leading spaces. split() with no arguments ignores the extra spaces. join glues the pieces with hyphens. replace(" ", "") removes all the spaces, the inner ones too. And "MENU" < "Menu" is True because the capital E (69) comes before the lower-case e (101).
Solution 2.
def title_summary(title):
"""Return one line with the normalised title and its statistics."""
clean = " ".join(title.split()).capitalize()
words = clean.split()
initials = ".".join([w[0].upper() for w in words]) + "."
return f"{clean} | {len(clean)} chars | {len(words)} words | {initials}"
print(title_summary(" book fair poster artwork "))
# Book fair poster artwork | 24 chars | 4 words | B.F.P.A.The key is to compute clean only once and measure on it: if you counted the characters of the original you would get 28 because of the extra spaces. The initials combine a comprehension from 05-01 (w[0].upper() for each word) with join, and the final dot is added separately because join only puts separators between elements.
Solution 3.
total = 0
print(f"{'TITLE':<24}{'ASSIGNEE':>10}{'PRIOR.':>10}{'DAYS':>6}")
for line in lines:
fields = line.strip().split(";")
if len(fields) != 4 or not fields[3].strip().isdigit():
print(f"Discarded: {line.strip()}")
continue
title = " ".join(fields[0].split()).capitalize()
assignee = fields[1].strip().capitalize()
priority = fields[2].strip().upper()
days = int(fields[3].strip())
total += days
print(f"{title:<24}{assignee:>10}{priority:>10}{days:>6}")
print(f"{'TOTAL':<24}{total:>26}")Two valid rows come out —"Book fair poster / Luis / HIGH / 3" and "Sole bakery menu / Marta / MEDIUM / 5", 8 days in total— and two discards: the third line has only three fields and the fourth brings "two" where a number should go. Two details that hold for any data reader. The check len(fields) != 4 or not fields[3].strip().isdigit() gathers the two reasons for discarding into a single guard, and the continue from 03-03 avoids nesting the rest of the body inside an if. And every field is cleaned with .strip() before being used, because the second line brings spaces around the semicolons: without that strip, " 5 ".isdigit() would be False and the good line would be discarded.
Conclusion
A string is an immutable sequence of characters: it accepts indices, slices and for traversals just like a list, but it cannot be modified, so all of its methods return a new string and you have to store it. You have been through the six families of methods: cleaning (strip, lstrip, rstrip), case (upper, lower, title, capitalize, casefold), searching (in, find, rfind, index, count, startswith, endswith), checking (isdigit, isalpha, isalnum, isspace), transformation (replace, zfill, center, ljust, rjust) and splitting and joining (split, rsplit, splitlines, join), which is the bridge between text and lists. You know how to write multiline text with """, protect yourself from escapes with r"", align columns with f-strings, and why "Zebra" < "apple" forces you to compare and sort on a lower-case version. And you know that accented letters and ñ are one character each, but that on its way to disk the text is encoded into bytes and that is why UTF-8 matters.
At Alba Studio you can now normalise the titles so that " BOOK fair " and "Book Fair" are the same text, generate codes such as ALB-SOL-01S and turn a line "Book fair poster;luis;high;3" into four fields ready to use. What remains unsolved is the problem 05-01 left open: those four fields are still four separate variables, or worse, four parallel lists that have to be kept in sync by hand. The solution arrives in Dictionaries and sets: a structure where every piece of data is stored with its name —title, assignee, priority, days, completed— instead of with a position number. There EasyTask will make the jump to v0.9 and a task will become, at last, a single thing.
Fundamentals of Programming
Module 1: Introduction to Programming
- What is programming?
- History of programming
- Programming languages
- Development environments
- From problem to algorithm
Module 2: Core Concepts
- Variables and data types
- Operators and expressions
- Input and output
- Type conversion and data validation
Module 3: Control Structures
Module 4: Functions and Procedures
- Defining and using functions
- Parameters and return values
- Variable scope
- Breaking a program down into functions
- Functions as values: lambda and higher order
Module 5: Data Structures
- Lists and arrays
- Strings
- Dictionaries and sets
- Tuples and nested structures
- Saving data to files: text, CSV and JSON
Module 6: Basic Algorithms
Module 7: Objects and Code Organisation
- From data to objects: classes and instances
- Attributes, methods and the constructor
- Collections of objects
- Modules, packages and imports
Module 8: Good Practices and Tools
- Documentation and comments
- Debugging and error handling
- Version control
- Automated testing
- Style, readability and refactoring
