◀ Course contents Part 1 · Module 1-04

Working with Text

Most real programs are mostly text handling

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.

"  Kenji  ".strip()          # "Kenji"
"KENJI".lower()              # "kenji"
"kenji tanaka".title()       # "Kenji Tanaka"
"a-b-c".replace("-", " ")    # "a b c"
"kenji@example.com".endswith(".com")   # True
"tea, rice".count(",")       # 1

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.

"tea,rice,miso".split(",")      # ["tea", "rice", "miso"]
"Kenji  Tanaka".split()         # ["Kenji", "Tanaka"]  — any whitespace
", ".join(["a", "b", "c"])      # "a, b, c"

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.

code = "PY-2026-KENJI"
code[0] → "P"
code[3:7] → "2026"
code[-5:] → "KENJI"

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
Python
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
    Python
    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
      Python
      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
        Python
        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
          Python
          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.

            "a,b,c".split(",")        →  ["a", "b", "c"]
            " ".join(["a", "b"]) → "a b"

            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
            Python
            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
              Python
              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
                Python
                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
                  Python
                  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_nameKenji Tanaka, capitalised as a name
                    • emailkenji@example.com, all lowercase
                    • cityOsaka
                    • initialsKT, built from the first letter of each part of the name
                    • domainexample.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
                    Python
                    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})").

                    Output
                    
                          

                      Notification