Strings show up in a huge share of interview problems: anagrams, palindromes, parsing, sliding windows. Python strings are immutable, compactly stored objects. Knowing how they're stored tells you which operations are cheap, which copy, and why one innocent-looking loop can turn code into .
How CPython stores a string#
Immutable. Once created, a str never changes. s.upper(), s.replace(...) and s + t all return a new string. Immutability is what makes strings safe to share between functions and usable as dict keys.
Fixed width per string (PEP 393). CPython picks the smallest width that fits the widest character in that string, and uses it for every character:
| Widest character in the string | Bytes per character |
|---|---|
| Latin-1 (U+0000 to U+00FF), e.g. plain ASCII | 1 |
| Up to U+FFFF, e.g. most CJK text | 2 |
| Beyond U+FFFF, e.g. emoji like 🐍 | 4 |
So appending one emoji to 1,000 ASCII characters gives a new string of about 4,000 data bytes, not 1,004. The pay-off is indexing: with uniform width, s[i] is just start + i * width. Mixed widths would force a scan from the start.
Length and hash are stored. len(s) reads a stored size field, so it's . The hash is computed the first time it's needed (for example, the first dict lookup) and cached in the object, so later lookups with the same string object skip rehashing.
Interning. CPython keeps one shared copy of some strings, mainly identifier-like literals ("hello_world") and names used in code. That lets the interpreter compare names by address. You can force it with sys.intern(s). It is an optimisation, not a guarantee, so compare strings with ==, never is.
What operations cost#
Here is what common string operations cost for a string of length :
| Operation | Cost | Note |
|---|---|---|
len(s), s[i] |
stored size, fixed-width chars | |
s[a:b] |
slicing copies into a new string | |
s + t |
always a new string | |
"".join(parts) |
one allocation | |
s == t |
may exit early on mismatch | |
sub in s, s.find(sub) |
about typical | can be worse for adversarial patterns |
hash(s) |
once, then cached | |
s[::-1], s.lower(), s.split() |
each returns new objects | |
sorted(s) |
returns a list of chars |
The += trap and the join fix#
Since strings can't grow, result += ch builds a brand-new string and copies everything so far into it, every iteration. Building a string of length that way copies about characters.
"".join(parts) scans the parts once to compute the total length, allocates once, then copies each piece straight into place: .
words = ["a"] * 5
# Slow pattern: new string each iteration
result = ""
for w in words:
result += w
# Fast pattern: collect in a list, join once
parts = []
for w in words:
parts.append(w) # amortised O(1)
fast = "".join(parts)
print(result == fast) # True
CPython sometimes resizes a string in place for s += t when nothing else references s, so the slow loop may look fine on your laptop. That's an implementation detail other interpreters don't share. In interviews, use a list plus join, and say why.
Quick check#
Test yourself on the ideas above.
| Question | Answer |
|---|---|
What does sub = s[0:5] do in memory? |
Allocates a new string and copies 5 characters. It is not a view into s. |
What is the time complexity of len(s)? |
: the length is stored in the object. |
| When is a string's hash computed? | The first time it's needed, then cached in the object. |
| Why can a string be a dict key but a list can't? | The string can't change, so its hash can't change either. |
Method reference#
Case conversion#
| Method | What it does | Example → Result |
|---|---|---|
s.lower() |
all lowercase | "AbC".lower() → "abc" |
s.upper() |
all uppercase | "AbC".upper() → "ABC" |
s.capitalize() |
first char upper, rest lower | "hELLO".capitalize() → "Hello" |
s.title() |
first letter of each word upper | "hi there".title() → "Hi There" |
s.swapcase() |
flip each char's case | "AbC".swapcase() → "aBc" |
s.casefold() |
aggressive lowercase for comparisons | "ß".casefold() → "ss" |
Searching#
| Method | What it does | Example → Result |
|---|---|---|
s.find(sub) |
index of first match, -1 if absent | "abcabc".find("c") → 2 |
s.rfind(sub) |
index of last match, -1 if absent | "abcabc".rfind("c") → 5 |
s.index(sub) |
like find but raises ValueError if absent |
"abc".index("b") → 1 |
s.rindex(sub) |
last match, raises if absent | "abcb".rindex("b") → 3 |
s.count(sub) |
non-overlapping occurrences | "aaa".count("aa") → 1 |
sub in s |
membership | "bc" in "abc" → True |
Checking (return bool)#
| Method | What it does | Example → Result |
|---|---|---|
s.startswith(pre) |
begins with prefix | "abc".startswith("ab") → True |
s.endswith(suf) |
ends with suffix | "abc".endswith("c") → True |
s.isdigit() |
all chars are digits | "123".isdigit() → True |
s.isalpha() |
all chars are letters | "abc".isalpha() → True |
s.isalnum() |
all letters or digits | "a1".isalnum() → True |
s.isspace() |
all whitespace | " ".isspace() → True |
s.islower() / s.isupper() |
all cased chars lower / upper | "abc".islower() → True |
s.isnumeric() |
all numeric, incl. Unicode | "½".isnumeric() → True |
Trimming#
| Method | What it does | Example → Result |
|---|---|---|
s.strip() |
remove leading and trailing whitespace | " hi ".strip() → "hi" |
s.lstrip() / s.rstrip() |
left only / right only | " hi".lstrip() → "hi" |
s.strip(chars) |
remove any of the given chars from both ends | "xxhixx".strip("x") → "hi" |
Splitting and joining#
| Method | What it does | Example → Result |
|---|---|---|
s.split() |
split on runs of whitespace | "a b c".split() → ["a","b","c"] |
s.split(sep) |
split on an exact separator | "a,b,c".split(",") → ["a","b","c"] |
s.split(sep, n) |
split at most n times |
"a,b,c".split(",", 1) → ["a","b,c"] |
s.rsplit(sep, n) |
split from the right | "a,b,c".rsplit(",", 1) → ["a,b","c"] |
s.splitlines() |
split on line breaks | "a\nb".splitlines() → ["a","b"] |
sep.join(items) |
join strings with a separator | ",".join(["a","b"]) → "a,b" |
Replacing and padding#
| Method | What it does | Example → Result |
|---|---|---|
s.replace(old, new) |
replace all occurrences | "aaa".replace("a","b") → "bbb" |
s.replace(old, new, n) |
replace the first n |
"aaa".replace("a","b",2) → "bba" |
s.zfill(w) |
pad left with zeros | "42".zfill(5) → "00042" |
s.ljust(w, c) / s.rjust(w, c) |
pad right / left to width | "hi".ljust(5,"*") → "hi***" |
s.center(w, c) |
pad both sides | "hi".center(6,"*") → "**hi**" |
Partitioning#
| Method | What it does | Example → Result |
|---|---|---|
s.partition(sep) |
(before, sep, after) at first match |
"a=b=c".partition("=") → ("a","=","b=c") |
s.rpartition(sep) |
same, at last match | "a=b=c".rpartition("=") → ("a=b","=","c") |
Interview idioms#
These one-liners come up again and again in string problems.
from collections import Counter
s = "listen"
print("".join(sorted(s))) # 'eilnst': anagram signature
print(s[::-1]) # 'netsil': reversed copy
print(ord("c") - ord("a")) # 2: char -> 0..25 index for count arrays
print(chr(ord("a") + 3)) # 'd': index -> char
print(Counter(s)["l"]) # 1: character frequencies
chars = list(s) # mutable copy for in-place style edits
chars[0] = "L"
print("".join(chars)) # 'Listen'
Gotchas#
Watch out for these common string mistakes.
- Every "modifying" method returns a new string.
s.strip()on its own line does nothing useful; writes = s.strip(). findvsindex:findreturns -1 when absent,indexraises. Watch forif s.find(x):, which is falsy when the match is at index 0.split()vssplit(" "): baresplit()collapses runs of whitespace and drops leading/trailing space."a b".split()→["a","b"], but"a b".split(" ")→["a","","b"].- Slices copy. In a loop,
s[i:]costs each time. Pass indices to recursive helpers instead of slicing. - String hashes vary between runs (hash randomisation), so never rely on
hash(s)being stable across processes.
Key takeaways#
- Strings are immutable; every change makes a new object.
- Each string uses 1, 2 or 4 bytes per char depending on its widest char, which keeps indexing .
lenis , slicing and concatenation copy, and the hash is cached after first use.- Build strings with a list and
"".join, not+=in a loop. - Compare strings with
==; interning is an implementation detail.