Strings
Python’s str is an immutable sequence of Unicode code points. The type was unified across the Python 2-to-3 transition: in Python 3, every str is Unicode, and bytes is a separate type for raw byte data. The combination — Unicode by default, explicit byte handling, multiple literal forms — is the substance of Python’s text-handling story. Modern Python’s f-strings (PEP 498, since 3.6) admit compact and readable string formatting; the older %-style and .format() mechanisms remain available.
The str type
A str is an immutable sequence of Unicode code points:
s = "hello"
s[0] # 'h'
s[1:3] # 'el'
len(s) # 5
"e" in s # True
s + " world" # "hello world" (a new str; s is unchanged)
s * 3 # "hellohellohello"
Strings are immutable: every “modification” produces a new string. The implication for performance: string concatenation in a loop allocates per iteration. The defence is "".join(...) or a list accumulator:
# Slow:
s = ""
for word in words:
s = s + word + " "
# Fast:
s = " ".join(words)
Python internally optimises the simple s += part case in CPython since 2.6, but the explicit join is the conventional choice.
Indexing and slicing
s = "hello"
s[0] # 'h'
s[-1] # 'o' (negative index from the end)
s[1:4] # 'ell' (slice)
s[:3] # 'hel'
s[2:] # 'llo'
s[::-1] # 'olleh' (reverse)
s[::2] # 'hlo' (every other character)
Slicing produces a new string. The slice notation [start:stop:step] is consistent across all sequence types.
String literals
Python admits several literal forms:
Regular literals
s1 = "hello"
s2 = 'hello' # single quotes are equivalent
s3 = "she said \"hi\""
s4 = 'she said "hi"' # avoid escaping by using the other quote
# Triple-quoted (multi-line):
multi = """line1
line2
line3"""
The single-quote and double-quote forms are interchangeable; the choice is stylistic. Triple-quoted strings admit embedded newlines without \n escapes.
Escape sequences
"\n" # newline
"\t" # tab
"\\" # literal backslash
"\"" # literal double quote
"\xff" # byte 0xff (in a string context, character U+00FF)
"é" # 'é' (4-digit Unicode)
"\U0001f600" # '😀' (8-digit Unicode)
"\N{GREEK SMALL LETTER ALPHA}" # 'α' (named Unicode character)
"\0" # null character
The full list of escapes is in the language reference.
Raw strings
The r prefix turns off escape processing:
path = r"C:\Users\alice"
regex = r"\d{4}-\d{2}-\d{2}"
Raw strings are the conventional choice for regular expressions and Windows file paths — any string with substantial backslash content.
The exception: a raw string cannot end with an odd number of backslashes (r"a\" is a syntax error).
Byte strings
The b prefix produces bytes:
b = b"hello" # bytes, not str
b[0] # 104 (the byte value, an int)
Combinations:
rb"…"orbr"…"— raw bytes.f"…"— formatted string (treated below).- Combinations like
rf"…"— raw f-string.
The u"…" prefix (Unicode string) is admitted but redundant in Python 3; every string is already Unicode.
f-strings (formatted string literals)
Python 3.6 introduced f-strings — formatted string literals with embedded expressions:
name = "Alice"
age = 30
greeting = f"Hello, {name}, age {age}"
# "Hello, Alice, age 30"
pi = 3.14159
formatted = f"pi: {pi:.2f}"
# "pi: 3.14"
# Expressions inside the braces:
nums = [1, 2, 3]
summary = f"sum: {sum(nums)}, max: {max(nums)}"
# Nested braces for literal braces:
template = f"{{key}}: {value}" # literal {key}: value
# Format specifiers:
hex_value = f"{255:#x}" # "0xff"
padded = f"{42:>10}" # " 42" (right-aligned, width 10)
percent = f"{0.123:.1%}" # "12.3%"
The expression inside {} is evaluated at runtime; the result’s __format__ method produces the formatted string. The format specifier (after :) follows the Format Specification Mini-Language (the same one used by str.format() and format()).
Python 3.12 admitted nested expressions in f-strings (the { and } inside the expression no longer need careful escaping in some cases); the mechanism is mature and widely used.
f-strings are the conventional contemporary form for string formatting.
The .format() method
The older formatting mechanism:
"{} is {}".format("alice", 30) # "alice is 30"
"{0} or {1}, {0}".format("yes", "no") # "yes or no, yes" (positional)
"{name} is {age}".format(name="alice", age=30) # "alice is 30" (named)
"{:>10}".format("hi") # " hi" (format spec)
"{:.2f}".format(3.14159) # "3.14"
The .format() form predates f-strings and is still supported. It is the conventional form for templates loaded from configuration or i18n files (where the format string is not a literal):
template = load_template("greeting") # "Hello, {name}!"
result = template.format(name="alice")
For new code with literal strings, f-strings are preferable.
The % formatting (legacy)
The oldest formatting mechanism:
"%s is %d" % ("alice", 30) # "alice is 30"
"%(name)s is %(age)d" % {"name": "alice", "age": 30}
"%.2f" % 3.14159 # "3.14"
The % form is similar to C’s printf. It is uncommon in modern Python; new code uses f-strings.
String methods
str has a substantial set of methods:
| Method | Effect |
|---|---|
s.upper(), s.lower() | Case conversion |
s.title(), s.capitalize(), s.swapcase() | Other case conversions |
s.strip(chars), s.lstrip(), s.rstrip() | Strip whitespace or specified chars |
s.startswith(prefix), s.endswith(suffix) | Prefix/suffix tests |
s.find(sub), s.rfind(sub) | Position; -1 if not found |
s.index(sub), s.rindex(sub) | Position; raises ValueError if not found |
s.count(sub) | Count of non-overlapping occurrences |
s.replace(old, new, count) | Replace |
s.split(sep), s.rsplit(sep, maxsplit) | Split into list |
s.splitlines(keepends) | Split on line boundaries |
s.join(iterable) | Join an iterable of strings |
s.partition(sep), s.rpartition(sep) | Split into three parts |
s.center(width, fill), s.ljust(width), s.rjust(width) | Padding |
s.zfill(width) | Pad with zeros (for numbers as strings) |
s.translate(table) | Apply a translation table |
s.encode(encoding) | Convert to bytes |
s.format(*args, **kwargs) | Format |
s.isdigit(), s.isalpha(), s.isalnum(), s.isspace() | Predicates |
s.isupper(), s.islower(), s.istitle() | Case predicates |
The full list is in the standard library; the conventional ones are split, join, replace, strip, format, and the predicates.
# Tokenise:
words = "alice bob carol".split() # ['alice', 'bob', 'carol']
fields = "a=1,b=2,c=3".split(",") # ['a=1', 'b=2', 'c=3']
# Join:
csv = ",".join(["a", "b", "c"]) # "a,b,c"
sentence = " ".join(words)
# Replace:
fixed = "hello world".replace("world", "python")
# Strip:
clean = " hello ".strip() # "hello"
# Predicates:
"123".isdigit() # True
"hello".isalpha() # True
Encoding and decoding
str is Unicode; bytes is raw bytes. Conversion requires an explicit charset:
# str → bytes:
"hello".encode("utf-8") # b'hello'
"héllo".encode("utf-8") # b'h\xc3\xa9llo'
"hello".encode("ascii") # b'hello'
"héllo".encode("ascii") # UnicodeEncodeError
# bytes → str:
b"hello".decode("utf-8") # 'hello'
b"h\xc3\xa9llo".decode("utf-8") # 'héllo'
b"\xff\xfe".decode("utf-8") # UnicodeDecodeError
The conventional choices:
- UTF-8 — the dominant encoding for new data.
- latin-1 (ISO-8859-1) — for legacy systems and as a one-to-one byte-to-character mapping.
For tolerant decoding, the errors argument:
b"\xff\xfe".decode("utf-8", errors="replace") # 'ÿþ' (substitution)
b"\xff\xfe".decode("utf-8", errors="ignore") # '' (skip invalid)
b"\xff\xfe".decode("utf-8", errors="strict") # default; raises
Comparison and ordering
String comparison is lexicographic on Unicode code points:
"abc" < "abd" # True
"abc" < "abcd" # True (shorter prefixes are smaller)
"Z" < "a" # True (uppercase code points are smaller)
"abc" == "abc" # True
"abc" == "abc".lower() # True
The comparison is not locale-aware. For locale-aware comparison, use locale.strcoll (or external libraries like pyicu).
For case-insensitive comparison:
"ABC".lower() == "abc".lower() # True
"ABC".casefold() == "abc" # True; better for some Unicode
import unicodedata
unicodedata.normalize("NFC", "café") == unicodedata.normalize("NFC", "café")
casefold() is the conventional contemporary case-insensitive comparison; lower() is sufficient for ASCII.
Multi-line strings and line handling
text = """
This is a
multi-line string.
"""
for line in text.splitlines():
process(line)
# Or with a generator-friendly form:
for line in text.split("\n"):
process(line)
The splitlines() method handles platform-specific line endings (CR, LF, CRLF) and is the conventional choice over split("\n").
Translation tables
For substantial character substitutions, str.translate with a translation table:
table = str.maketrans("aeiou", "*****")
"hello world".translate(table) # "h*ll* w*rld"
# Or removing characters:
table = str.maketrans("", "", "aeiou")
"hello world".translate(table) # "hll wrld"
The mechanism is faster than repeated replace calls for many substitutions.
Regular expressions
Python’s regex support is in the re module:
import re
# Match:
match = re.match(r"\d+", "abc 123")
if match:
print(match.group()) # nothing; match starts from the beginning
match = re.search(r"\d+", "abc 123")
if match:
print(match.group()) # "123"
# Find all:
re.findall(r"\d+", "1 and 2 and 3") # ["1", "2", "3"]
# Replace:
re.sub(r"\d+", "X", "1 and 2 and 3") # "X and X and X"
# Compile for repeated use:
pattern = re.compile(r"\d{4}-\d{2}-\d{2}")
pattern.search("date: 2024-01-15")
The full treatment is in the standard library; for non-trivial regex, a compiled pattern is conventionally preferable to repeated re.search calls.
A note on bytes and bytearray
For byte-level operations:
b = b"hello"
b[0] # 104 (an int)
chr(104) # 'h'
bytes([104, 101, 108, 108, 111]) # b'hello'
# Mutable variant:
ba = bytearray(b"hello")
ba[0] = ord("H")
bytes(ba) # b'Hello'
bytes is the immutable byte type; bytearray is the mutable variant. The conventional uses are protocol implementation, file I/O, and any byte-level work.
Common patterns
String building with join
parts = []
for item in items:
parts.append(format_item(item))
result = "\n".join(parts)
The join form is faster than repeated + concatenation for substantial sequences.
f-string with conditionals
status = "ok"
message = f"Status: {status if status else 'unknown'}"
The {} admits any expression, including conditionals.
Multi-line f-string
report = f"""
Report
------
Name: {name}
Total: {total:,}
Date: {date:%Y-%m-%d}
"""
The triple-quoted f-string admits formatted multi-line output.
Tokenising
import re
# Simple split:
words = text.split() # whitespace
csv_fields = csv_line.split(",")
# Regex-based split:
tokens = re.split(r"[\s,]+", text) # whitespace or comma
Reading lines
with open("input.txt") as f:
for line in f:
process(line.rstrip()) # strip trailing newline
The for line in f is the conventional iteration; each line includes its newline (which rstrip() removes).
A note on Unicode
Python’s str is Unicode throughout; the language handles non-ASCII text by default. The substantial discipline:
- In-memory text —
str(Unicode). - On-disk and on-wire —
byteswith explicit encoding/decoding. - Encoding for new data — UTF-8.
- Iteration over
str— by code point (not byte, not grapheme cluster).
For grapheme-aware iteration (combining characters, emoji), libraries (grapheme, regex) are needed. For most application work, Python’s built-in Unicode handling is sufficient.