Skip to main content

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.

String methods
MethodDescription
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