Programming with PythonPython Setup and First Programs › Day 45

Day 45: Strings and Text Processing

Day 45 of 365 — Strings and Text Processing

After this lesson you will handle text in Python with confidence — creating, slicing, cleaning, and formatting strings — and understand why strings are immutable Unicode sequences and how they become the bytes that travel to files and services.

Course
Programming with Python
Category
Python Setup and First Programs
Reading time
≈ 40 min
Practical time
≈ 30 min
Lesson duration
1h 10m
Last verified
2026-07-12

Hands-on lab for this lesson

Lab files on GitHub: https://github.com/ai-roadmap-365/ai-roadmap-365.github.io/tree/main/labs/sections/programming-with-python/day-045-strings-and-text-processing

  1. Get the hands-on files. Clone the labs repository once (you can reuse this clone for every lesson). This works on macOS, Linux, and Windows (PowerShell or WSL):
    git clone https://github.com/ai-roadmap-365/ai-roadmap-365.github.io.git
    cd ai-roadmap-365.github.io
  2. Open this lesson's lab. Move into the directory for this specific day. Every lab lives at the same predictable path — section / subsection / week / day:
    cd labs/sections/programming-with-python/day-045-strings-and-text-processing
  3. Read the lab guide. Open `README.md` in that directory. It lists the exact commands, what each does, the expected output, and how to check your work — read it before running anything.
  4. Run it and check your work. Follow the README's "How to run" section: run the example first to see the finished result, then complete the numbered exercises in `starter/`, then run the tests. The tests pass (exit 0) only when your work is correct.
    bash tests/run_tests.sh   # or the test command named in the lab README

You can also open the lab as a local page (works offline, shows the file tree and expected output).

Learning objectives

By the end of this lesson you will be able to:

Prerequisites

Why this matters

Almost everything you will ever say to an AI system, and almost everything it says back, is a string of text. A prompt is a string. A model’s reply is a string. The instructions you write, the documents you feed in for context, the labels on your training data, the configuration you paste into a terminal — all strings. Long before a model does anything clever with language, plain, ordinary code has to build that text, clean it, cut it apart, and stitch it back together. If you cannot do that fluently, everything downstream is harder than it needs to be.

The practical consequences are immediate. When you assemble a prompt from a template — a fixed frame with a few blanks filled in from your data — you are doing string formatting, and a single misplaced quote or a stray newline can change the answer you get. When you load a dataset of customer reviews or scraped web pages, most of your effort is not modelling at all; it is text cleaning — stripping whitespace, fixing inconsistent capitalization, splitting fields, throwing away junk. Practitioners often repeat that data preparation is the majority of real machine-learning work, and the majority of data preparation is string work. Even the way a model reads your prompt starts with a step called tokenization, which chops a string into pieces before any mathematics happens.

Today you learn how Python represents and manipulates text: strings as immutable sequences of characters, how to slice them, the handful of methods you will use every day, the three styles of formatting (and which one to reach for), and how text becomes the bytes that travel over a network or sit on a disk. By the end you will read and write string code without hesitation — the quiet skill underneath every noisy demo.

The idea in plain language

A string in Python is a piece of text: a sequence of characters in a fixed order. You write one by putting characters between quotes — "hello" or 'hello' — and Python stores it as an object you can inspect and copy from, but never change in place. That last part is the single most important idea in this lesson: strings are immutable. Once a string exists, its characters are settled. Every operation that looks like it “changes” a string actually builds and hands back a brand-new string, leaving the original exactly as it was. This is the same idea of immutability you began meeting when you studied Python’s types on Day 44, now applied to text.

Because a string is a sequence, it has positions, and you can reach into it the way you reached into a list: s[0] is the first character, s[-1] is the last. You can also take a run of characters — a slice — with s[start:stop], and Python hands you a new, smaller string. On top of positions, strings carry a rich set of methods: named actions you call with a dot, like s.lower() to get a lower-cased copy or s.split() to break the text into a list of words. And when you need to build a sentence out of values — a name, a count, a price — you use string formatting to drop those values into a template, with f-strings being the modern, readable way to do it.

Hold three facts in mind and the rest follows: a string is an ordered sequence you can index and slice; a string never changes, so methods return new strings; and formatting is how you weave data into text. Everything else in this lesson is detail hung on those three hooks.

Historical background

Text handling is older than Python by decades, and its history explains why Python does things the way it does. In the earliest computers there were only numbers; text was bolted on by agreeing that particular numbers would stand for particular characters. The most influential such agreement, ASCII, was standardized in 1963 and assigned the numbers 0 through 127 to the English letters, digits, punctuation, and a few control codes — so the capital letter A became the number 65, exactly the convention you met on Day 5 when you learned how a computer represents data. The word “string” itself — a “string of characters” — has been standard computing vocabulary since those early years.

ASCII’s 128 slots covered American English and almost nothing else. As computing spread across languages, a tangle of incompatible encodings grew up, each mapping the bytes above 127 to different accented letters or scripts, so text that looked fine on one machine turned to garbage on another. The fix was Unicode: a single catalogue that gives every character in every writing system its own number, or code point. The Unicode Consortium was founded in 1991 to steward it. A companion invention, UTF-8, designed by Ken Thompson and Rob Pike in 1992, encodes those code points into bytes efficiently and has become the dominant text encoding of the internet.

Python grew up alongside Unicode and eventually embraced it fully. The release of Python 3.0 in December 2008 made a decisive change: an ordinary string (str) became a sequence of Unicode characters, while raw bytes got their own separate type (bytes). This clean split — text is one thing, bytes are another, and you convert deliberately between them — is why Python 3 handles the world’s languages so gracefully, and why the encode/decode pair you will meet below matters. The last piece of the modern picture arrived with Python 3.6 in December 2016, which introduced f-strings through PEP 498 (authored by Eric V. Smith): a concise, fast, readable syntax for embedding values directly inside string literals. Every technique in this lesson sits on that history.

What it is — and what it is not

A Python string is an immutable sequence of Unicode characters, represented by the built-in type str. Every word is load-bearing. Immutable: you cannot alter a character in place. Sequence: it has an order and a length, so indexing, slicing, iteration, and len() all work. Unicode characters: each element is a full character — a letter, a digit, an emoji, a Chinese ideograph — not a raw byte, so len("café") is 4 regardless of how many bytes that would take to store.

It helps to be equally clear about what a string is not. A string is not a bytes object: "hello" (text) and b"hello" (bytes) are different types, and mixing them raises errors on purpose. A string is not mutable like a list, so s[0] = "H" is not allowed and never will be. A string is not a number, even when it looks like one: "42" is text, and adding it to 8 is an error until you convert it with int("42"). And a string method never edits the original — a frequent beginner trap is calling s.upper() and expecting s to change, when in fact you must capture the returned value: s = s.upper().

MisconceptionThe reality
s.upper() changes the string.”It returns a new upper-cased string; the original is untouched. You must assign the result.
len(s) counts bytes.”It counts characters (code points). "café" has length 4 even though UTF-8 stores it in 5 bytes.
”Single and double quotes mean different things.”They are interchangeable; pick whichever avoids escaping the quote inside your text.
\n is two characters, a backslash and an n.”It is one character, a newline. The backslash is an escape that Python reads as a single control character.
”You need a regex to do any text work.”Most everyday tasks are simpler and clearer with plain string methods; regex is for genuinely complex patterns (Day 38).

Why it was created and what problems it solves

Strings exist because computers compute on numbers, but humans think in words, names, sentences, and files full of prose. Some layer has to bridge that gap, letting a program hold “the customer’s last name” or “the first line of the file” as a single, manipulable value. Without a string type you would juggle bare arrays of character-numbers by hand, tracking lengths and boundaries yourself — exactly the error-prone drudgery that older languages forced on programmers and that caused a long history of bugs and security holes.

The immutability of Python strings solves a subtler set of problems. Because a string can never change underneath you, it is safe to share: you can pass a string to a function, use it as a dictionary key, or hand the same string to ten different parts of a program, all without worrying that one of them will secretly rewrite it and surprise the others. Immutable text is predictable text. The cost — that “editing” a string means building a new one — is real but usually cheap, and Python’s design bets, correctly, that safety and simplicity are worth more than in-place edits for the vast majority of programs. The rich library of string methods and the evolution toward f-strings then solve the everyday problem of ergonomics: making the common tasks — clean this up, split that apart, format this nicely — short, readable, and hard to get wrong.

How it works

Let’s build the picture from the ground up: how you create strings, how you reach into them, the methods you will lean on, how you format output, and how text turns into bytes.

Creating strings

You write a string literal between single or double quotes, and the two are interchangeable — choose the one that lets you avoid escaping:

a = 'She said hello'
b = "It's a fine day"     # double quotes so the apostrophe needs no escape

For text that spans several lines, use triple quotes, which preserve the line breaks inside:

poem = """the river and the reader
a river starts as a thin trickle"""

Some characters cannot be typed directly, so Python uses escape sequences: a backslash followed by a code. The important ones are \n (newline), \t (tab), \\ (a literal backslash), and \" or \' (a quote that would otherwise end the string). Each escape is a single character in the resulting string, even though you typed two symbols. When you want the backslashes to stay literal — most commonly for Windows file paths or regular-expression patterns — prefix the string with r to make a raw string, where r"C:\new" really is a backslash, an n, an e, and a w, rather than a newline.

Indexing and slicing

Because a string is a sequence, each character has a position, counted from 0 at the left. Negative positions count from the right, so -1 is the last character. A slice s[start:stop] copies the run from start up to but not including stop, returning a new string of stop - start characters. Omit an end to run to the edge (s[:3], s[3:]), and add a third number — the step — to skip characters or, with -1, walk backwards: s[::-1] reverses the whole string.

Diagram: indexing and slicing a Python string with positive and negative positions and one slice highlighted

The diagram traces the word "PYTHON". Reading it fixes the two rules people most often stumble over: the start index is included and the stop index is excluded (so s[1:4] gives three characters, "YTH"), and negative indices let you reach the end without knowing the length. Because slicing returns a fresh string, none of this ever disturbs the original — immutability again.

Essential methods

A string method is an action you call with a dot after the string. Because strings are immutable, every method that transforms text returns a new string; the original stays put. These few cover the great majority of real work:

MethodWhat it doesExample → result
s.lower() / s.upper()Return a copy in lower or upper case"River".lower()"river"
s.strip()Remove leading/trailing whitespace (or given characters)" hi \n".strip()"hi"
s.split(sep)Break into a list; default splits on any whitespace"a b c".split()["a", "b", "c"]
sep.join(list)Join a list of strings with sep between them"-".join(["a","b"])"a-b"
s.replace(old, new)Replace every occurrence of one substring with another"a.b.c".replace(".", "/")"a/b/c"
s.find(sub)Position of the first match, or -1 if absent"banana".find("na")2
s.startswith(p) / s.endswith(p)Test the beginning or end; returns True/False"report.txt".endswith(".txt")True
s.count(sub)How many non-overlapping times sub appears"banana".count("a")3

The pair split and join are the workhorses of text processing: split turns a line into a list of fields you can examine, and join turns a list of pieces back into one string. Notice that join is a method on the separator, which reads oddly at first (", ".join(names)) but is exactly right — the separator is what goes between the pieces.

String formatting: three styles

Formatting means weaving values into text. Python has three styles, added over its history, and knowing when to use each is part of A13’s tool literacy — all three are built into the language, entirely free, with no package to install.

The oldest is the % operator, borrowed from the C language: "Total: %.2f" % price. The second is the str.format() method: "Total: {:.2f}".format(price), using {} placeholders. The newest and now the default is the f-string: put an f before the opening quote and write the value directly inside braces — f"Total: {price:.2f}". Inside those braces you can place any expression, not just a variable, and after a colon you can add a format spec that controls how the value is displayed.

Flowchart: an f-string broken into literal text, an embedded expression, and a format spec producing formatted output

The diagram takes f"Total: {price * qty:.2f}" apart. Python keeps the literal text Total: as-is, evaluates the expression price * qty first, then applies the format spec .2f — “a floating-point number with two decimal places” — to display the result. With price = 5.0 and qty = 5, the whole f-string produces Total: 25.00. Format specs are a small language of their own: :>10 right-aligns a value in a field ten characters wide, :<10 left-aligns it, :^10 centres it, :, inserts thousands separators, and :.1% shows a fraction as a percentage. You will use alignment specs to line up columns in a table, exactly as today’s lab does.

StyleLooks likeUse it when
f-stringf"Hi {name}, {n:,} items"Almost always — most readable, fastest, expressions inline (Python 3.6+)
str.format()"Hi {}, {:,} items".format(name, n)The template and its values are far apart, or you build the template dynamically
% operator"Hi %s, %d items" % (name, n)Reading old code, or a codebase that already uses it; avoid in new code

Unicode and encoding in Python

A str is a sequence of Unicode characters — abstract letters, independent of how they are stored. But files and networks deal in bytes, so at the boundary you must convert. Turning text into bytes is encoding (s.encode("utf-8")), and turning bytes back into text is decoding (b.decode("utf-8")). This is the same idea you met on Day 5, now with names: an encoding like UTF-8 is the agreed rule for which bytes represent which characters.

text = "café"
data = text.encode("utf-8")   # b'caf\xc3\xa9'  — 5 bytes
back = data.decode("utf-8")   # "café"          — 4 characters

Notice that the four-character string takes five bytes, because UTF-8 stores the accented é in two bytes. This is why len() on a string counts characters, not storage. The one firm rule: always name the encoding explicitly when you read or write text, rather than relying on the platform default, which differs between operating systems and is a classic source of the dreaded UnicodeDecodeError. Today’s lab passes encoding="utf-8" for exactly this reason.

A nod to regular expressions

When a pattern is genuinely complex — “any sequence of digits,” “an email-shaped token,” “whitespace of any kind” — plain methods stop being enough, and you reach for regular expressions (regex), Python’s re module, which you met on Day 38. Regex is powerful but harder to read, so the professional habit is to prefer simple string methods for simple jobs and escalate to regex only when the pattern truly demands it. We return to this trade-off below.

An everyday analogy

Picture a label-maker that prints words onto a strip of tape — a printed ribbon of characters. Each character sits in its own little cell along the ribbon, in a fixed order, and that is your string.

Reading the ribbon is easy and safe: you can point at the first cell (s[0]), the last cell (s[-1]), or count in from either end. You can take scissors and cut out a run of cells to get a shorter piece — but here is the crucial detail that makes the analogy honest: your “cut” is really a photocopy of that run. The original ribbon is never damaged; slicing hands you a fresh copy and leaves the source intact. That is immutability. You physically cannot re-ink a single cell on an existing ribbon; if you want a different message, the machine prints a new ribbon. So when you “change” a string — upper-case it, strip its edges, replace a word — the label-maker is quietly printing a new tape and handing it to you, while the old one still sits on the desk exactly as before.

The methods are the machine’s buttons. One button copies the ribbon in all capitals; another trims the blank tape off the ends; another cuts the ribbon at every space to give you a little pile of word-strips; another glues a pile of strips back into one ribbon with a chosen separator between them. And formatting is the template mode: you set up a ribbon with blanks in it — “Total: ____” — and the machine fills each blank with a value, formatted just so, printing the finished tape. Keep the printed ribbon in mind and the rules stop feeling arbitrary: you can read any cell, copy any run, and press buttons to print new ribbons, but you can never re-ink a cell in place.

Examples in practice

Start with the technical heart of today’s lab: counting words in a block of text using only splitting and a dictionary.

text = "a river runs, a river rests, a river runs on"
counts = {}
for token in text.split():             # ["a", "river", "runs,", ...]
    word = token.strip(".,").lower()   # tidy the edges, normalize case
    counts[word] = counts.get(word, 0) + 1
# counts == {"a": 3, "river": 3, "runs": 2, "rests": 1, "on": 1}

Read it slowly. text.split() cuts the line into tokens on whitespace. For each token, strip(".,") peels commas and periods off the ends and lower() folds case, so "runs," and "runs" count as the same word — a tiny but typical piece of text cleaning. The dictionary tallies each word, and counts.get(word, 0) supplies a starting count of 0 the first time a word appears. This exact pattern — split, normalize, tally in a dict — recurs constantly whenever you summarize text.

Now formatting, building an aligned row for a report:

label, value = "Words", 105
print(f"{label:<20}{value:>10}")   # 'Words                      105'

The :<20 left-aligns the label in a 20-character field and :>10 right-aligns the number in a 10-character field, so a column of such rows lines up neatly no matter how long each label is. Change value to 1234567 and the number still sits flush against the right edge. This is precisely how today’s lab prints its statistics table.

A few more everyday tasks, each a one-liner:

"  ready\n".strip()                       # "ready"        — clean whitespace
"first,last,email".split(",")             # ['first', 'last', 'email']  — parse a CSV row
"/".join(["usr", "local", "bin"])         # "usr/local/bin"  — build a path
"report_2026.txt".endswith(".txt")        # True            — check a file type
"the river".replace("river", "sea")       # "the sea"       — substitute a word
"Hello, World"[7:]                         # "World"         — slice off a prefix

Each of these — cleaning, parsing, building, checking, substituting, slicing — is a routine step in preparing text, and together they cover a surprising share of the code you will write when wrangling data.

Implications: security, privacy, performance, scalability, and cost

Security. The great classical vulnerabilities are string-handling failures: building a database query or a shell command by pasting untrusted text straight into a template lets an attacker inject their own commands. The lesson for your own code is to keep untrusted input as data, never splicing it blindly into something that will be executed, and to use the safe, parameterized tools your libraries provide rather than hand-built string concatenation for queries and commands.

Privacy. Text is where sensitive information lives — names, addresses, messages, health details. Because a string is easy to copy, log, and print, personal data can leak simply by being written to a log file or echoed in an error message. When you clean and process text, be deliberate about what you retain and what you print; a stray print of a whole record is a real privacy incident.

Performance. Immutability has a cost: building a big string by repeatedly adding to it (result = result + piece in a loop) creates a new string every time and can turn a fast job slow. The idiomatic fix leans on the very methods in this lesson — collect the pieces in a list and "".join(them) once at the end, which builds the final string in a single pass. Knowing this one pattern spares you a common performance trap.

Scalability. When text grows from a line to a multi-gigabyte file, you cannot always hold it all in memory as one string. The scalable habit is to process text a line or a chunk at a time — iterating over a file yields one line at a time — so memory use stays flat no matter how large the input. The string operations are identical; you simply apply them to a stream of pieces rather than one giant string.

Cost. In systems that price work by the amount of text — including the language models you will use, which meter usage by tokens — the length and cleanliness of your strings translate directly into money and speed. Trimming needless whitespace, removing boilerplate, and sending only the text that matters are not just tidy habits; they reduce cost and latency. Efficient text handling is efficient spending.

Alternatives: free, open source, and commercial

Every tool in this lesson ships inside Python itself, at no cost, so “alternatives” means the different built-in approaches and the standard-library modules that extend them — not paid products.

ApproachTypeWhat it offersCost
f-strings (str and its methods)Built into PythonThe default for creating and formatting text; readable and fastFree
str.format() and %Built into PythonOlder formatting styles you will meet in existing codeFree
re (regular expressions)Standard libraryPattern matching for complex text (Day 38)Free
string moduleStandard libraryConstants like string.punctuation; Template for simple $name substitutionFree
textwrapStandard libraryWrapping and filling paragraphs to a widthFree
unicodedataStandard libraryLook up and normalize Unicode charactersFree
pandas string methods (.str)Open-source libraryApply string operations across whole columns of a tableFree/open source

For the work in this course you will spend almost all your time with str methods and f-strings, reach for re when a pattern is genuinely complex, and later meet pandas string methods when you clean tabular data at scale. There is no paid tier to any of this; text processing in Python is free all the way down.

Concept AConcept BKey difference
str (text)bytes (raw bytes)str is Unicode characters for humans; bytes is storage/transmission. Convert with encode/decode.
String methodsRegular expressions (re)Methods are simple and readable for fixed substrings; regex handles complex, variable patterns at the cost of readability.
f-stringstr.format()Same power; f-strings embed the expression inline and read better, so they are the default for new code.
Slicing s[a:b]Indexing s[i]Indexing returns one character; slicing returns a new (possibly empty) substring, b - a characters long.
String (immutable)List (mutable)You can reassign a whole string but not edit one character in place; a list can be changed element by element.
splitjoinInverse operations: split turns one string into a list of pieces; join turns a list of pieces into one string.

When to use it — and when not to

Reach for plain string methods whenever the task is describable with fixed substrings and simple structure: trimming whitespace, changing case, splitting on a known separator, checking a prefix or suffix, replacing a literal word. This is the overwhelming majority of text work, and methods keep it short and obvious. Reach for f-strings whenever you build output for a person or a template for a machine — reports, messages, prompts, file names — because they are the clearest way to weave values into text. Reach for encode/decode precisely at the boundary where text meets a file, a socket, or any raw-byte world, and name the encoding every time.

Know when to escalate and when to stop. When a pattern becomes genuinely variable — matching any run of digits, any whitespace, an email-shaped token — stop stretching string methods into knots and switch to a regular expression (Day 38), which was built for exactly that. Conversely, do not reach for regex when a one-line replace or split would do; a regex is harder to read and easier to get subtly wrong, and reviewers will thank you for the simpler code. And when performance matters, do not build a large string by repeated concatenation in a loop — collect pieces and join them once. The professional instinct is to use the simplest tool that clearly expresses the task, and to escalate only when the task, measured honestly, demands it.

The AI thread ties all of this together. When you engineer a prompt, you are formatting a string — often an f-string template with your data dropped into the blanks. When you prepare training or evaluation data, you spend most of your time on string cleaning: stripping, splitting, normalizing case, removing junk. When a model reads your input, the very first step is tokenization, which operates on the string you hand it, so the exact characters — every stray space and newline — matter. And when you send text to any service that charges by length, tidy strings are cheaper strings. The fluency you build today is not a preliminary to the interesting work; in AI practice, careful text handling is a large part of the work.

Knowledge check

Try these from memory before looking back:

  1. Explain in one sentence why s.upper() does not change s, and what you must do to keep the upper-cased result.
  2. Given s = "PYTHON", state the values of s[0], s[-1], s[1:4], and s[::-1], and say how many characters s[1:4] contains and why.
  3. Write an f-string that prints a name left-aligned in 12 columns followed by a price right-aligned in 8 columns with two decimal places.
  4. Describe the difference between str and bytes, and name the two operations that convert between them in each direction.
  5. You must decide between a string method and a regular expression for a task: give one task where each is the right choice, and justify both.

Hands-on exercise

Time to process real text. In the Day 45 lab you will build a small program, text_report.py, that reads a fixed block of sample text and produces a clean report using only the tools from this lesson: a title-cased heading, counts of lines, words, characters, and unique words, the single most common word (found with split plus a dictionary), and a neatly aligned statistics table built with f-string format specs. The full solution lives in the lab’s examples/ directory; a starter with five numbered exercises is in starter/.

To see the finished report first, then start your own version, run these from the lab directory:

python3 examples/text_report.py
python3 starter/text_report.py

The first command prints the complete report from the committed sample; the second runs your working copy, which begins with the five exercises unfinished. Each exercise names the exact method or expression to use — read the file, build the heading with .strip().title(), count words into a dict, find the most common, and print the table with alignment specs.

Expected output

Running the completed reference program on the committed sample prints exactly this (the sample never changes, so your numbers must match):

============================================
          The River And The Reader          
============================================

Preview:  the river and the reader A river starts ...
Heading reversed:  redaeR ehT dnA reviR ehT
First / last body word:  'river' / 'you'

Statistic                Value
------------------------------
Lines                        9
Words                      105
Characters                 556
Unique words                66

Most common word:  "river" (11 times, 10.5% of words)

The heading is the first line of the sample fed through .title(); the reversed line is heading[::-1]; the table rows use :<20 and :>10 to align; and the final line reports the word your counting dictionary found most often.

Validate your work

You are done when you can check every box:

Troubleshooting

Common mistakes

Practice assignment

Complete the strings worksheet in the lab’s starter/ directory and finish all five exercises in starter/text_report.py so it reproduces the expected report exactly. Then extend the program in one small way of your choosing and write two or three sentences in the worksheet about what you added: for example, print the longest line and its length using max(lines, key=len) and an f-string, or print the top three words instead of only the most common by sorting counts.items() and slicing [:3]. Record in the worksheet the sample’s word count, one slice expression with its result, and one f-string you wrote yourself that uses both an embedded expression and a format spec. Keep the worksheet; a later lesson on input and output builds directly on today’s formatting.

Extension challenge

Go one layer deeper into the text-versus-bytes distinction that underlies all of this. In a Python shell, take a string that contains a non-ASCII character — s = "café" — and compare len(s) with len(s.encode("utf-8")). Explain in writing why the two differ, which one counts characters and which counts bytes, and what would happen if you tried to decode those bytes with the wrong encoding. Then measure the performance point from the lesson: build a 100,000-character string two ways — once by repeated concatenation in a loop (result = result + "x") and once by collecting the pieces in a list and calling "".join(pieces) — and time both with Python’s timeit. Write two or three sentences on which was faster and why immutability explains the difference. You will have connected today’s abstract idea — strings are immutable Unicode sequences — to concrete facts about memory, speed, and encoding that working practitioners reason about every day.

Quiz

Q1. You run `s = "river"; s.upper()` and then print `s`. What does it show, and why?

  1. "RIVER", because upper() changes the string in place
  2. "river", because upper() returns a new string and the original is unchanged
  3. An error, because strings have no upper() method
  4. "River", because upper() only capitalizes the first letter
Show answer

Answer: B. "river", because upper() returns a new string and the original is unchanged

Strings are immutable, so `upper()` returns a brand-new upper-cased string and leaves `s` untouched. To keep the result you must assign it: `s = s.upper()`.

Q2. What is `len("café")` in Python 3, and what does the number count?

  1. 5, because it counts the bytes needed to store the text
  2. 4, because it counts characters (code points), not bytes
  3. 3, because accented letters are not counted
  4. It depends on the operating system
Show answer

Answer: B. 4, because it counts characters (code points), not bytes

A Python 3 `str` is a sequence of Unicode characters, so `len` counts characters: "café" has 4. Its UTF-8 encoding happens to take 5 bytes because `é` needs two, but `len` on the string counts characters.

Q3. Given `s = "PYTHON"`, what is `s[1:4]`?

  1. "YTHO"
  2. "PYT"
  3. "YTH"
  4. "YTHON"
Show answer

Answer: C. "YTH"

A slice `s[start:stop]` includes `start` but excludes `stop`, so `s[1:4]` takes indices 1, 2, and 3 — "YTH" — which is `4 - 1 = 3` characters long.

Q4. Which formatting style is the recommended default for new Python code?

  1. The f-string, e.g. `f"Total: {price:.2f}"`
  2. The `%` operator, e.g. `"Total: %.2f" % price`
  3. Manual concatenation with `+` and `str()`
  4. The `%` operator for numbers and `+` for text
Show answer

Answer: A. The f-string, e.g. `f"Total: {price:.2f}"`

Introduced in Python 3.6 (PEP 498), f-strings are the modern default: they embed the expression inline, read clearly, and are fast. `%` and `str.format()` still work but are preferred mainly for older code.

Q5. Which operation turns a `str` into `bytes`, and what must you always supply?

  1. `decode`, always supplying a file path
  2. `str()`, which needs nothing
  3. `encode`, always supplying the encoding such as "utf-8"
  4. `bytes()` with no arguments
Show answer

Answer: C. `encode`, always supplying the encoding such as "utf-8"

Text is turned into bytes with `encode` (`s.encode("utf-8")`) and bytes back into text with `decode`. You should always name the encoding explicitly rather than rely on the platform default, which is a common cause of `UnicodeDecodeError`.

Q6. What does `"-".join(["a", "b", "c"])` produce?

  1. ["a", "-", "b", "-", "c"]
  2. "a-b-c"
  3. "-a-b-c-"
  4. "abc"
Show answer

Answer: B. "a-b-c"

`join` is a method on the separator: it glues the list elements into one string with the separator placed *between* them, giving "a-b-c". It is the inverse of `split`.

Q7. What is the value of `"river"[::-1]`?

  1. "river"
  2. "rev ir"
  3. "revir"
  4. "revir" reversed to "rivre"
Show answer

Answer: C. "revir"

A slice with a step of `-1`, `s[::-1]`, walks the string backwards and returns a new reversed string, so "river" becomes "revir".

Q8. For which task is a plain string method the better choice over a regular expression?

  1. Matching any run of one or more digits anywhere in the text
  2. Checking whether a filename ends with the fixed suffix ".txt"
  3. Extracting every email-shaped token from a document
  4. Matching whitespace of any kind, including tabs and newlines
Show answer

Answer: B. Checking whether a filename ends with the fixed suffix ".txt"

A fixed-substring test like "ends with .txt" is exactly what `str.endswith(".txt")` is for — simple and readable. The other three involve variable patterns, which are where regular expressions (Day 38) earn their added complexity.

Glossary

string
A piece of text in Python: an ordered, immutable sequence of Unicode characters, written between quotes and represented by the built-in type str.
immutable
Unable to be changed in place. A string never changes; every operation that seems to modify it actually returns a new string and leaves the original untouched.
index
The position of a single character in a string, counted from 0 at the left; negative indices count from the right, so s[-1] is the last character.
slice
A run of characters copied out of a string with s[start:stop], returning a new string that includes start but excludes stop (stop − start characters).
method
A named action called on a string with a dot, such as s.lower() or s.split(); because strings are immutable, transforming methods return a new string.
f-string
A string literal prefixed with f whose {...} braces embed expressions evaluated at run time, the modern default for formatting (added in Python 3.6 via PEP 498).
format spec
The part of an f-string field after the colon that controls display, such as :.2f for two decimal places or :>10 to right-align in a 10-character field.
split
A string method that breaks text into a list of pieces on a separator; with no argument it splits on any run of whitespace.
join
A string method called on a separator that glues a list of strings into one, placing the separator between the pieces; the inverse of split.
encode
The operation that turns a str into bytes using a named encoding such as UTF-8, done at the boundary where text is written to a file or network.
decode
The operation that turns bytes back into a str using a named encoding; the reverse of encode.
escape sequence
A backslash followed by a code that represents a single character hard to type directly, such as \n for a newline or \t for a tab.
raw string
A string literal prefixed with r in which backslashes are treated literally rather than as escapes, useful for file paths and regular-expression patterns.

Sources and further reading


Kept in this browser, no account needed. Your progress page turns the whole record into one link you can bookmark or open on another device.