Python Strings
Data Types Focus
Python Strings
Strings store Unicode text and are immutable. Master literals, slicing, methods, f-string formatting, and encoding.
Literals and escape sequences
single = 'hello'
double = "hello" # Single and double quotes are equivalent
multiline = """This spans
multiple lines"""
raw = r"C:\path\file" # Raw string: backslashes stay literal
Escape sequences let you embed special characters:
"line 1\nline 2" # \n = newline
"col 1\tcol 2" # \t = tab
"She said \"hi\"" # \" = literal quote
"C:\\path" # \\ = literal backslash
Reach for raw strings (r"...") with Windows paths and regular expressions, where backslashes would otherwise need doubling.
Strings are immutable
A string can never be changed in place. Every "modifying" method returns a new string and leaves the original untouched.
s = "python"
s.upper() # 'PYTHON' — a new string
s # 'python' — unchanged
s[0] = "P" # TypeError: 'str' object does not support item assignment
To "change" a string, reassign the result:
s = s.replace("p", "P") # 'Python'
This immutability is why strings are hashable and can be used as dictionary keys.
Indexing and slicing
Strings are sequences, so they support the same indexing and slicing as lists.
s = "python"
s[0] # 'p' first character
s[-1] # 'n' last character
s[0:3] # 'pyt' characters 0, 1, 2
s[3:] # 'hon' from index 3 to the end
s[::-1] # 'nohtyp' reversed
len(s) # 6
Each slice returns a new string. To test membership or iterate:
"tho" in s # True
for char in s:
print(char)
Concatenation and repetition
"py" + "thon" # 'python' — join with +
"ab" * 3 # 'ababab' — repeat with *
Building a string from many pieces with += in a loop is slow because each step allocates a new string. Use str.join instead:
parts = ["a", "b", "c"]
"-".join(parts) # 'a-b-c' — one allocation, much faster in loops
Common methods
Because strings are immutable, every method below returns a new string rather than modifying the original.
| Method | Description |
|---|---|
| strip() | Remove whitespace at both ends (lstrip/rstrip for one side) |
| lower() / upper() | Change case |
| replace(old, new) | Substitute every occurrence |
| split(delimiter) | Split into a list (splits on whitespace if omitted) |
| join(iterable) | Concatenate an iterable with a delimiter |
| startswith()/endswith() | Prefix/suffix check |
| find(sub) | Index of first match, or -1 if not found |
| count(sub) | Count non-overlapping occurrences |
| removeprefix()/removesuffix() | Strip a prefix/suffix if present (Python 3.9+) |
| isdigit() / isalpha() | True if all characters are digits / alphabetic |
Formatting with f-strings
An f-string (prefix f) evaluates {expressions} inline. Prefer them over .format() and % for readability and speed.
name = "Ada"
age = 32
f"{name} is {age}" # 'Ada is 32'
f"{name.upper()} in {age * 2} years" # any expression works
A format specifier after a colon controls how the value renders:
price = 1234.5
f"{price:.2f}" # '1234.50' — 2 decimal places
f"{price:,.2f}" # '1,234.50' — thousands separator
f"{price:>10}" # ' 1234.5' — right-align in 10 columns
f"{0.25:.0%}" # '25%' — percentage
f"{255:#x}" # '0xff' — hexadecimal
Add = to print an expression and its value—handy for quick debugging:
f"{age=}" # 'age=32'
Encoding and bytes
Strings are Unicode text. To send text over a network or write raw bytes to a file, encode it to bytes; decode it back on the way in.
text = "café"
data = text.encode("utf-8") # b'caf\xc3\xa9'
data.decode("utf-8") # 'café'
Mismatched encodings raise UnicodeDecodeError. Handle unknown or malformed input explicitly:
data.decode("utf-8", errors="replace") # substitutes � for bad bytes
data.decode("utf-8", errors="ignore") # drops bad bytes
Default to UTF-8 unless a source dictates otherwise.
Next up in your learning path
Frequently Asked Questions
Are Python strings mutable?
No. Strings are immutable—methods like upper() or replace() return a new string and leave the original unchanged. Assign the result back to keep the change. Immutability is also what makes strings usable as dictionary keys.
s = "python"
s.upper() # 'PYTHON' — a new string
s # 'python' — unchanged
s = s.replace("p", "P") # reassign to keep the change
How do I reverse a string in Python?
Use an extended slice with a step of -1. This works because strings are sequences.
"python"[::-1] # 'nohtyp'
Should I use f-strings, .format(), or %?
Prefer f-strings. They are the most readable and the fastest, and they support inline expressions and format specifiers. Use .format() only when the template and values are defined separately; the % operator is legacy.
name, age = "Ada", 32
f"{name} is {age}" # preferred
"{} is {}".format(name, age) # older, still fine
"%s is %s" % (name, age) # legacy
What is the difference between a string and bytes?
A str holds Unicode text; bytes holds raw binary data. Files and network sockets work in bytes, so you encode on the way out and decode on the way in.
data = "café".encode("utf-8") # b'caf\xc3\xa9'
data.decode("utf-8") # 'café'
Why is joining faster than concatenating with +?
Because strings are immutable, each += in a loop builds a brand-new string, which is O(n²) overall. join() allocates the result once, so prefer it when combining many pieces.
parts = ["a", "b", "c"]
"-".join(parts) # 'a-b-c' — one allocation