All 24 modules are open from the start — nothing here is
locked, and nothing costs anything. Sign in so your progress, titles and
credentials stay with you, on every device you use.
Free forever, with your Google account. No password, no payment.
Names, emails, addresses, log lines, form fields, file contents — the
majority of what a program touches is text that arrived in the wrong shape.
This module covers cutting text apart, cleaning it up, putting it back
together, and formatting a result someone can read.
Ready?
1
Every Character Has a Position
A string is a sequence of characters, each with a position counted from
zero. Not one. Zero.
word = "DOJO"
# 0123
word[0] # "D"
word[3] # "O"
word[-1] # "O" — negative counts back from the end
len(word) # 4
Because counting starts at zero, the last position is
len(word) - 1, never len(word). Reaching for the
latter gives you an IndexError, and that off-by-one is the
single most common mistake in this module.
A slice takes a range: word[start:stop]
includes the start position and stops before the stop position.
[2:5]
A range
Positions 2, 3 and 4. Never 5.
[:3]
From the start
The first three characters.
[3:]
To the end
Everything from position 3 onwards.
[-4:]
The last four
Counting back from the end.
Why stop-before is the right rule
It looks arbitrary until you notice two things. text[:n]
and text[n:] always rejoin into exactly the original, with
nothing lost or duplicated. And the length of text[a:b] is
simply b - a. Both stop being true if the stop position is
included.
Quick check
Given code = "PY-2026", what does code[3:7] give you?
2
Methods: Functions Attached to a Value
A method is a function that belongs to a value. You call
it with a dot, and the value it is attached to is the thing it works on.
Every one of these hands back a new string. None of them
changes the original, because strings in Python are
immutable — they cannot be modified in place, ever.
Does nothing
name.upper()
On its own line. The new string is created and immediately thrown away.
Works
name = name.upper()
The result is assigned somewhere, so it survives.
Methods can be chained, because each one returns a string
that the next can be called on. Read them left to right, as a sequence of
steps: messy.strip().title() is "remove the outer spaces,
then fix the capitals".
Quick check
A program runs email.strip() on its own line, then compares email to a stored address and finds no match. Why?
3
Breaking Apart and Putting Back Together
split() cuts a string into a list of pieces.
join() glues a list of pieces back into one string. They are
exact opposites and they turn up together constantly.
split() with nothing in the brackets is a special case worth
knowing: it breaks on any run of whitespace and discards empty pieces, so
it copes with double spaces, tabs and line breaks without you thinking
about it.
join() reads backwards
Everyone gets this the wrong way round at first. The string you call
join() on is the glue, and the list of
pieces goes inside the brackets. So ", ".join(parts) means
"put a comma and a space between every piece". Not
parts.join(", ") — that is the other language you are
thinking of.
The reason it is worth the awkwardness: join() puts the
separator between pieces, never after the last one. Building the
same line by hand almost always leaves a trailing comma.
Quick check
You have parts = ["a", "b", "c"] and want "a-b-c". Which line does it?
4
f-strings: Writing the Sentence, Dropping in the Values
Put an f before the opening quote and anything inside
{braces} is replaced by its value:
name = "Kenji"
done = 3
f"{name} cleared {done} modules" # "Kenji cleared 3 modules"
f"{name} has {24 - done} left" # "Kenji has 21 left"
The braces hold a whole expression, not just a name, so a small
calculation can live right where its result belongs.
After a colon comes a format spec, which controls how the
value is displayed. These three cover most of what you will need:
:.2f
Two decimal places
f"{37.5:.2f}" gives 37.50. This is how money prints.
:>6
Right-align in 6
Pads on the left, which is what lines numbers up in a column.
:,
Thousands separator
f"{1234567:,}" gives 1,234,567.
A debugging trick worth stealing
Put an = at the end of the expression inside the braces and
Python prints the expression as well as its value:
f"{total=}" produces total=37.5. When a program
is doing something you did not expect, scattering a few of those beats
guessing every time.
Once a sentence has more than one value in it, reach for an f-string.
Quick check
You need a total of 7.5 to print as 7.50. What goes in the braces?
0 of 9 completed
Real Python runs right here in your browser — nothing to install, nothing
sent to a server. The interpreter downloads once the first time you press
Run, then stays cached.
01
To do
Every character in a string has a position, counted from
zero. word[0] is the first character.
word[2:5] is a slice: start at 2, stop
before 5. Leaving a side blank means "all the way" —
word[:3] is the first three characters and
word[3:] is everything from position 3 onwards. Negative
positions count back from the end, so word[-1] is the last
character.
Your task: from ticket, pull out
prefix (the first two characters), year (the
four digits) and name (everything after the last dash).
Print each on its own line.
your_code.py
PythonCtrl↵ to run
Hint
Count the positions: P is 0, Y is 1, the dash is 2, so the year starts at 3. ticket[3:7] gets the four digits, and ticket[-5:] grabs the last five characters.
Output
02
To do
A string cannot be modified once it exists. Assigning to a position —
code[-1] = "I" — raises a TypeError, and no
amount of trying will make it work.
This sounds like a limitation and is closer to a guarantee: a string you
were handed cannot be changed underneath you by anything else. What you
do instead is build a new one and point the name at it, which is
why every string method hands a value back rather than editing.
code = code[:-1] + "I" # rebuild it code = code.replace("KENJl", "KENJI") # or ask a method
Your task: the last character of this ticket code is a
lowercase L where it should be a capital I. Run the code, read the error,
then fix it so it prints PY-2026-KENJI.
your_code.py
PythonCtrl↵ to run
Hint
Take everything except the last character with code[:-1], stick "I" on the end, and assign the whole thing back to code.
Output
03
To do
Text from humans arrives messy: stray spaces, inconsistent capitals, the
wrong separators. String methods fix that. A method is a
function attached to a value, called with a dot:
" Kenji ".strip() → "Kenji" (spaces off both ends) "KENJI".lower() → "kenji" "kenji".upper() → "KENJI" "kenji".title() → "Kenji" "a-b".replace("-", " ") → "a b"
Every one of these hands back a new string. The original
is untouched — strings in Python can never be modified in place, which is
why you always assign the result to something.
Your task: turn messy into
clean: no spaces at either end, and capitalised as a name.
The result must be exactly Kenji Tanaka.
your_code.py
PythonCtrl↵ to run
Hint
Two problems, two methods, and they chain: messy.strip() removes the outside spaces, and .title() fixes the capitals. Write it as one expression, left to right.
Output
04
To do
A phone number arrives as +81 (90) 1234-5678. Before it can
be stored or compared with another one, every character that is not a
digit has to go.
replace(old, new) replaces every occurrence, and
replacing something with "" deletes it. Each call hands back
a new string, so they chain:
raw.replace(" ", "").replace("-", "")
Your task: strip the plus, the spaces, both brackets and
the dash out of raw into digits, then print the
number and how many digits it has:
819012345678 12 digits
your_code.py
PythonCtrl↵ to run
Hint
Five characters have to go: + and a space and ( and ) and -. Chain a .replace(x, "") for each one, left to right, on a single line.
Output
05
To do
Half of what you do with text is not changing it but asking about it.
Four questions cover most of the ground:
"2026" in path → True (is it in there anywhere?) path.startswith("reports/") → True path.endswith(".csv") → True path.find("summary") → 16 (-1 if it is not there)
The first three hand back True or False, which
is exactly what an if wants in the next part.
find() hands back a position, and -1 rather
than an error when there is nothing to find.
Your task: a file has landed in a storage bucket and
something has to decide what to do with it. From path,
create is_csv, folder (everything before the
first slash) and has_2026, then print three lines:
csv: True folder: reports 2026: True
your_code.py
PythonCtrl↵ to run
Hint
path.endswith(".csv") answers the first. For the folder, path.split("/") cuts it into three pieces and [0] takes the first. And "2026" in path is a complete expression on its own — it already produces True.
Output
06
To do
split() cuts a string into a list of pieces, and
join() glues a list back into one string.
join() reads backwards the first few times: the string you
call it on is the glue, and the list goes inside the brackets.
", ".join(parts) means "put a comma and a space between every
piece".
Your task:raw holds three tags separated by
commas. Split them into tags, then build
headline by joining them with " · " (a
space, a middle dot, a space). Print the number of tags, then the
headline.
your_code.py
PythonCtrl↵ to run
Hint
tags = raw.split(",") gives you the list. Then " · ".join(tags) glues them back with the separator you want. len(tags) counts them.
Output
07
To do
split(",") cuts on exactly what you give it.
split() with nothing in the brackets is different: it cuts on
any run of whitespace and throws away the empties, so two spaces and
seven spaces both count as one separator. Log lines are padded to line up
on screen, which makes this the only sane way to read them.
The message at the end contains spaces of its own, so cutting on every
space would shred it. maxsplit stops after a set number of
cuts and leaves the rest in one piece:
line.split(maxsplit=2) → three parts, the last one whole
Your task: pull stamp, level
and message out of the log line and print them one per line:
2026-09-02 ERROR db timeout after 30s
your_code.py
PythonCtrl↵ to run
Hint
parts = line.split(maxsplit=2) gives you exactly three pieces, whatever the padding. Then parts[0], parts[1] and parts[2].
Output
08
To do
Building a sentence out of values with commas and plus signs gets ugly
fast. An f-string lets you write the sentence and drop
the values in where they belong. Put f before the opening
quote and any {expression} inside gets replaced by its value.
name = "Kenji" done = 12 f"{name} has {done} modules" → "Kenji has 12 modules"
Anything can go in the braces, including a calculation:
f"{done * 9} exercises". A format spec after a colon controls how a
number is displayed — f"{value:.2f}" shows exactly two
decimal places, which is how you print money without a mess of digits.
Your task: build summary as an f-string
reading exactly Kenji completed 3 modules and 27 exercises,
where the 27 is calculated rather than typed. Then build
price_line reading exactly Total: 37.50, with
two decimal places. Print both.
your_code.py
PythonCtrl↵ to run
Hint
f"{name} completed {modules} modules and {modules * per_module} exercises" — the maths can live inside the braces. For the money, f"Total: {total:.2f}".
Output
09
To do
The bit after the colon in an f-string is a format spec,
and it does more than decimal places. Give it a width and an alignment
and you get a column:
f"{name:<14}" → left-aligned, padded to 14 characters f"{qty:>3}" → right-aligned in 3 f"{price:>9,}" → right-aligned in 9, with thousands separators
Numbers are right-aligned so the digits line up under each other; text is
left-aligned so it reads. That is the whole convention, and it is why a
report built this way is scannable and one built with commas is not.
Your task: print three rows using the same f-string
layout: name left-aligned in 14, quantity right-aligned in 3, price
right-aligned in 9 with a thousands separator.
Tea 3 1,250 Matcha bowl 1 4,800 Whisk 12 990
your_code.py
PythonCtrl↵ to run
Hint
The first line is written for you. Copy it twice and change the three values: "Matcha bowl", 1, 4800 and then "Whisk", 12, 990.
Output
Tidy up a sign-up
To do
A sign-up form has handed you one messy line. Pull it apart, clean each
piece, derive two more from what you find, and print a record card whose
labels line up.
The raw value is " kenji TANAKA | KENJI@Example.COM | Osaka "
— three fields separated by a vertical bar, with random spacing and
capitals.
Create these five variables:
full_name — Kenji Tanaka, capitalised as a name
email — kenji@example.com, all lowercase
city — Osaka
initials — KT, built from the first letter of each part of the name
domain — example.com, taken from the email
Then print exactly four lines, using f-strings:
Name Kenji Tanaka (KT) Email kenji@example.com Domain example.com City Osaka
Each label is left-aligned in a nine-character column, so the values start
in the same place on every row. Use the format spec for that rather than
typing spaces, which stop lining up the first time a label changes.
Every field must be derived from raw. Nothing on the card may
be retyped.
your_code.py
PythonCtrl↵ to run
Hint
Each piece from split still has spaces around it, so .strip() every one, then .title() the name and .lower() the email. Split the cleaned name to get the two initials, and split the email on "@" to get the domain. For a row: print(f"{'Name':<9}{full_name} ({initials})").