Part 1 · Foundations · Module 03
Strings & Text
Names, messages, files, web pages, data: most of what programs handle is text. This module teaches you to slice, search, clean, and format text with confidence, introduces Python 3.14's new template strings for safe HTML and SQL, and explains how text really becomes bytes through Unicode.
- Write string literals with quotes, escapes, raw strings, and triple quotes
- Index and slice strings, including negative indexes and steps
- Explain string immutability and use the core methods to search and clean text
- Format output with f-strings and the format specification mini-language
- Use Python 3.14 template strings (
t"...") and explain why they make HTML and SQL safer - Explain Unicode code points, and convert between
strandbyteswith encode/decode
String literals
A string (type str) is an immutable sequence of characters. A
sequence is an ordered collection you can index into; you will meet lists and tuples later. You
write a string literal between single or double quotes; they are identical, so choose whichever avoids escaping:
a = 'single'
b = "double"
c = "It's easy" # double quotes let you use an apostrophe
d = 'She said "hi"'
e = """Triple quotes
span several lines."""
Escape sequences
A backslash \ starts an escape sequence, a way to write characters that are hard to
type: \n is a newline, \t a tab, \\ a literal backslash, \" a
quote, and \N{SNAKE} or \U0001F40D a character by its Unicode name or number.
Raw strings
Prefix a string with r to make it raw: backslashes are kept as-is. That is handy for
Windows paths and regular expressions.
Adjacent literals are joined automatically, and + joins (concatenates) strings at runtime;
* repeats them: "ab" * 3 is "ababab".
Remember: quotes are interchangeable; \ escapes; r"..." turns escapes off.
Indexing and slicing
Each character in a string has a position called its index. Indexes start at
0, so the first character is s[0]. Negative indexes count from the end:
s[-1] is the last character.
s[1:4] takes everything between
boundary 1 and boundary 4, which is "YTH". Negative indexes (bottom) count back from the end.A slice s[start:stop:step] extracts a substring. It includes start and
excludes stop, which is why s[0:3] has exactly 3 characters. Leave out
start to begin at the start and stop to go to the end. A step of 2 takes
every second character, and a negative step goes backwards, so s[::-1] reverses a string.
Remember: indexes start at 0; slices include the start, exclude the stop; [::-1]
reverses.
Immutability and string methods
Strings are immutable: once created, a string never changes. s[0] = "J" raises
TypeError: 'str' object does not support item assignment. Every operation that seems to modify a string
actually returns a new string. If you want to keep the result, assign it.
Notice that after line 2, name still has its spaces. Only when we rebind name
on the last line does it change, and even then the old string object was not modified; the name just points
somewhere new.
The methods you will use daily
A method is a function attached to an object, called with a dot: text.upper().
| Method | What it does | Example → result |
|---|---|---|
strip(), lstrip(), rstrip() | Remove whitespace (or given characters) from the ends | " hi ".strip() → "hi" |
lower(), upper(), title() | Change case | "ada".title() → "Ada" |
casefold() | Aggressive lowercase for case-insensitive comparison | "Straße".casefold() → "strasse" |
split(sep) | Break into a list of pieces | "a,b,c".split(",") → ['a', 'b', 'c'] |
sep.join(items) | Glue pieces with a separator | "-".join(["a", "b"]) → "a-b" |
replace(old, new) | Replace every occurrence | "a-b".replace("-", "+") → "a+b" |
find(sub) | Index of first match, or -1 | "hello".find("l") → 2 |
startswith(), endswith() | Test the beginning or end | "data.csv".endswith(".csv") → True |
count(sub) | Count non-overlapping occurrences | "banana".count("a") → 3 |
sub in s | Membership test (an operator, not a method) | "nan" in "banana" → True |
f-strings in depth
An f-string (formatted string literal) starts with f and can contain expressions
inside braces { }. Python evaluates each expression and inserts its text:
name, age = "Ada", 36
print(f"{name} is {age} and will be {age + 1} next year.")
The format specification mini-language
After the expression you may add a colon and a format spec, a tiny language that controls how the
value is displayed. The general shape is {value:[fill][align][width][,][.precision][type]}:
| Spec | Meaning | Example → result |
|---|---|---|
.2f | Fixed-point, 2 decimals | f"{3.14159:.2f}" → 3.14 |
, or _ | Thousands separators | f"{1234567.891:,.2f}" → 1,234,567.89 |
>8, <8, ^8 | Right, left, center in width 8 | f"{'hi':^8}|" → " hi |" |
*^10 | Fill character then alignment | f"{'hi':*^10}" → ****hi**** |
08b, x | Zero-padded binary, hex | f"{42:08b}" → 00101010 |
.1% | Percentage | f"{0.256:.1%}" → 25.6% |
e | Scientific notation | f"{12345.678:.2e}" → 1.23e+04 |
Self-documenting expressions with =
Putting = after the expression prints the expression text too. It is the quickest debugging tool
there is:
Conversion flags !r and !s apply repr() or str() before
formatting. !r shows quotes around strings, which helps spot stray spaces.
Nested format specs
Parts of the spec can themselves be expressions in braces, so the width or precision can be decided at runtime:
Older styles: str.format and %
You will see two older ways in existing code. "{} is {}".format(name, age) uses the same format spec
language, with positional ({0}) or named ({name}) fields, and is still useful when the
template is stored separately from the values, for example TEMPLATE.format(**data). The
"%s is %d" % (name, age) style is the oldest; recognize it, but prefer f-strings in new code.
Template strings: t"..." (new in 3.14)
Why: f-strings can be dangerous
Imagine building a web page from a user's comment with an f-string:
comment = "<img src=x onerror=steal(document.cookie)>"
page = f"<p>{comment}</p>" # the attacker's code now runs in every visitor's browser!
This is called an injection attack: untrusted text (text from a user or another system) is mixed into code (HTML, SQL) and gets interpreted as code. Cross-site scripting (XSS) in HTML and SQL injection in databases are among the most common security bugs in the world. The f-string has already merged everything into one string, so it is too late to tell which parts were trusted.
How template strings work
A Template remembers the pieces. Iterating over it yields, in order, the literal text segments
(plain str) and Interpolation objects. Each Interpolation has:
.value: the evaluated value.expression: the source text of the expression, such as"name".conversion:"r","s","a", orNone.format_spec: the spec after the colon, such as".2f", or""
The template also offers .strings (all text segments), .values, and
.interpolations as tuples.
A safe HTML renderer
Now we can write a function that escapes only the interpolated values, using html.escape to turn
< into < and so on. The literal parts, which the programmer wrote, are trusted
and kept as-is. (A function, defined with def, is a named block of reusable code; you
will study functions in depth in M05. Here, just read the body.)
The same idea makes SQL safe: a database library can turn each interpolation into a query parameter
instead of pasting the text into the query, so t"SELECT * FROM users WHERE name = {name}" can never be
hijacked by a name like x' OR '1'='1.
Unicode, encodings, and bytes
Computers store numbers, not letters. Unicode is the worldwide standard that gives every
character, in every writing system plus emoji, a number called a code point. "A" is 65,
"é" is 233, "🐍" is 128013. A Python str is a sequence of code points. ord(ch) gives a
character's code point and chr(n) goes the other way.
Encoding: from code points to bytes
Files and networks carry bytes (numbers from 0 to 255), not code points. An encoding is a rule for turning code points into bytes. UTF-8 is the dominant encoding on the web and in modern systems: it uses 1 byte for ASCII characters (plain English letters and digits) and 2 to 4 bytes for others.
text.encode("utf-8")turns astrintobytes.data.decode("utf-8")turnsbytesback into astr.
A bytes literal looks like b"abc". Bytes are immutable sequences of small integers:
indexing b"abc"[0] gives 97.
Garbled text like café is called mojibake. It always means bytes were decoded
with a different encoding than they were encoded with. The fix is to know, and state, your encoding.
Practice
Given word = "programming", use slicing (not typing the letters) to set
first3 to the first three characters, last3 to the last three, and
backwards to the whole word reversed.
Hint
word[:3], word[-3:], and a step of -1.
Show solution
word = "programming"
first3 = word[:3]
last3 = word[-3:]
backwards = word[::-1]
Set clean_name to raw_name with outer spaces removed and
each word capitalized ("Ada Lovelace"). Set clean_email to raw_email
stripped and lowercased. Set domain to the part of the clean email after the @.
Hint
Chain methods: raw_name.strip().title(). For the domain,
split("@") gives a list; take the item at index 1 (or use find with slicing).
Show solution
raw_name = " ada LOVELACE "
raw_email = " Ada@Example.COM "
clean_name = raw_name.strip().title()
clean_email = raw_email.strip().lower()
domain = clean_email.split("@")[1]
Using one f-string, set line so that the item name is left-aligned in
10 characters, then |, the quantity right-aligned in 3, then |, and the line total
(quantity × price) right-aligned in 8 with exactly 2 decimals. For the given values the result is
"coffee | 3| 13.50".
Hint
Specs: :<10, :>3, :>8.2f.
Show solution
item = "coffee"
qty = 3
price = 4.5
line = f"{item:<10}|{qty:>3}|{qty * price:>8.2f}"
print(line)
Complete render_html so that literal text is kept unchanged and every
interpolated value is formatted with its format spec and then escaped with html.escape. Fill in the
two lines marked ....
Hint
html.escape(format(part.value, part.format_spec)) for
interpolations; just part for text.
Show solution
import html
from string.templatelib import Template, Interpolation
def render_html(template: Template) -> str:
pieces = []
for part in template:
if isinstance(part, Interpolation):
pieces.append(html.escape(format(part.value, part.format_spec)))
else:
pieces.append(part)
return "".join(pieces)
name = "<i>Bob</i>"
print(render_html(t"<p>Hi {name}</p>"))
For text = "naïve café 🐍", set n_chars to its number of
code points and n_bytes to the number of bytes in its UTF-8 encoding. Then set
decoded to the string obtained by decoding data as UTF-8, and snake to the
code point number of "🐍".
Hint
len(text), len(text.encode("utf-8")),
data.decode("utf-8"), and ord.
Show solution
text = "naïve café 🐍"
data = b"\xc3\xa9t\xc3\xa9"
n_chars = len(text)
n_bytes = len(text.encode("utf-8"))
decoded = data.decode("utf-8")
snake = ord("🐍")
Mini-project: a text statistics tool
Quiz
Recap
- Strings are immutable sequences of Unicode code points; quotes are interchangeable,
r"..."is raw. - Indexes start at 0; negatives count from the end; slices include start and exclude stop.
- Methods return new strings:
strip,split/join,replace,find,startswith,casefold. - f-strings with format specs (
:>8.2f,:,,:.1%),=for debugging, and nested specs for runtime widths. - Python 3.14
t"..."produces aTemplateof strings andInterpolations, so values can be escaped or parameterized safely. - Encode
strtobytesand decode back with an explicit encoding, usually UTF-8.
Going further
- String methods in the standard library reference
- Format specification mini-language
- PEP 750: Template strings and
string.templatelib - Unicode HOWTO
- Python 3.14 reference for the course
- Next: M04 · Control Flow