Strings#
Strings are immutable sequences of Unicode code points. They appear in nearly every interview — parsing, two pointers, sliding window, hash map, and trie problems all start with comfortable string manipulation. This page covers creation, slicing, methods, formatting, encoding, and multiple solution approaches for common patterns.
How to use this page
Know slicing, split/join, and character indexing cold. Drill Interview traps before string-heavy mocks. Prerequisites: Python Language Fundamentals, Operators, Control Flow.
At a glance
| Track | Python Basics |
| Sections | 12 major topics |
| Outline | Use the right-hand TOC to jump |
Topics: What a Python string actually is · Creating strings — all forms · Indexing and slicing · Immutability in depth · Character and Unicode operations · String methods — comprehensive reference · String formatting — four generations · Comparisons and sorting · … (+4 more)
- What a Python string actually is
- Creating strings — all forms
- Indexing and slicing
- Immutability in depth
- Character and Unicode operations
- String methods — comprehensive reference
- String formatting — four generations
- Comparisons and sorting
- Encoding and decoding
- Regular expressions (
re) - Standard library helpers
- Interview patterns — multiple approaches
What a Python string actually is#
| Property | Implication |
|---|---|
| Immutable | Every "change" creates a new string |
| Sequence | Supports indexing, slicing, iteration, len |
| Unicode | str holds Unicode code points, not bytes |
| Hashable | Can be dict key / set element (if contents fixed) |
Internally CPython may use compact representations (1/2/4 bytes per character depending on content) — you don't control this, but never assume one char = one byte.
Creating strings — all forms#
s1 = "hello"
s2 = 'world' # equivalent quotes
s3 = """multi
line""" # preserves newlines
s4 = '''also
valid'''
s5 = str(42) # "42"
s6 = str([1, 2]) # "[1, 2]" — repr-like for some types
s7 = "x" * 5 # "xxxxx"
s8 = "-".join(["a", "b"]) # "a-b"
raw = r"C:\new\folder" # backslashes literal
raw2 = r"not a valid \ escape at end\" # SyntaxError — can't end with odd backslash
String prefixes#
| Prefix | Meaning | Example |
|---|---|---|
| (none) | Normal string | "hello" |
r / R |
Raw — \n is backslash + n |
r"\n" → '\\n' |
f / F |
f-string — embed expressions | f"{x}" |
fr / rf |
Raw f-string | fr"C:\{name}" |
b / B |
Bytes literal — not str | b"abc" |
u |
Legacy Unicode prefix (no-op in Py3) | u"hi" |
Concatenation — three approaches#
# 1. + operator — fine for 2-3 pieces
greeting = "Hello, " + name + "!"
# 2. join — O(n) total for many pieces (preferred)
"".join(parts)
"-".join(str(x) for x in nums)
# 3. f-string / format — when building formatted output
f"Hello, {name}!"
Interview trap — O(n²) concatenation
Indexing and slicing#
s = "python"
s[0] # 'p'
s[-1] # 'n' — last char
s[-2] # 'o'
s[1:4] # 'yth' — start inclusive, stop exclusive
s[:3] # 'pyt' — from beginning
s[3:] # 'hon' — to end
s[:] # full copy (new string)
s[::2] # 'pto' — step 2
s[::-1] # 'nohtyp' — reverse
s[1:10:2] # step applies across slice bounds
Out-of-range index raises IndexError; out-of-range slice returns empty string or partial:
Slicing vs indexing for interviews#
| Operation | Use when |
|---|---|
s[i] |
Single character access |
s[lo:hi] |
Substring extraction |
s[::-1] |
Reverse (creates copy) |
| Two pointers | In-place logic without copying |
Immutability in depth#
s = "hello"
# s[0] = "H" # TypeError
s = s.upper() # new string; name rebound
# original "hello" object may be garbage-collected if unreferenced
Methods that "modify" strings return new strings:
" hi ".strip() # "hi" — original unchanged
"a,b".split(",") # ["a", "b"]
"x".replace("x", "y") # "y"
Building strings from characters#
# Approach 1: list + join (preferred for mutable buffer)
chars = []
for ch in s:
if ch.isalpha():
chars.append(ch.lower())
result = "".join(chars)
# Approach 2: io.StringIO (rare in interviews)
from io import StringIO
buf = StringIO()
for ch in s:
buf.write(ch)
result = buf.getvalue()
Character and Unicode operations#
ord("A") # 65 — Unicode code point
chr(65) # "A"
ord("é") # 233
for i, ch in enumerate(s):
print(i, ch, ord(ch))
# ASCII letter → index 0-25
index = ord(ch.lower()) - ord("a")
# Category tests
"a".isalpha() # True
"3".isdigit() # True — any Unicode digit
"3".isdecimal() # stricter
" \t".isspace()
Case conversion#
"Hello".lower() # "hello"
"Hello".upper() # "HELLO"
"Hello".title() # "Hello"
"Hello".capitalize() # "Hello"
"Hello".casefold() # aggressive lower — best for case-insensitive compare
For case-insensitive equality: a.casefold() == b.casefold().
String methods — comprehensive reference#
Search and test#
| Method | Returns | On miss | Notes |
|---|---|---|---|
.find(sub) |
index | -1 |
O(n·m) naive |
.rfind(sub) |
last index | -1 |
|
.index(sub) |
index | ValueError |
|
.count(sub) |
int count | 0 |
|
.startswith(prefix) |
bool | tuple of prefixes OK | |
.endswith(suffix) |
bool | ||
sub in s |
bool | idiomatic membership |
"hello".find("ll") # 2
"hello".find("zz") # -1
"hello".index("ll") # 2
"hello".count("l") # 2
"file.txt".endswith((".txt", ".md"))
Split and partition#
| Method | Behavior |
|---|---|
.split(sep=None) |
Split on whitespace (default) or sep |
.split(sep, maxsplit) |
Limit splits |
.rsplit(sep, maxsplit) |
Split from right |
.splitlines(keepends=False) |
On \n, \r\n, etc. |
.partition(sep) |
(before, sep, after) — one split |
.rpartition(sep) |
From right |
"a b c".split() # ['a', 'b', 'c'] — any whitespace
"a,b,c".split(",") # ['a', 'b', 'c']
"key=value=extra".split("=", 1) # ['key', 'value=extra']
"key=value=extra".partition("=") # ('key', '=', 'value=extra')
When to use which:
split(",", 1)— exactly one delimiter (key=value)partition("=")— need separator in result tuplesplit()— tokenizing whitespace-separated input
Strip and padding#
" hello ".strip() # "hello"
" hello ".lstrip() # "hello "
"__hello__".strip("_") # "hello"
"42".zfill(5) # "00042"
"hi".center(10, "-") # "---hi-----"
"hi".ljust(10)
"hi".rjust(10)
Replace and translate#
"aba".replace("a", "x") # "xbx"
"aba".replace("a", "x", 1) # "xba" — max replacements
"hello".replace("l", "L", 2)
table = str.maketrans("aeiou", "12345")
"hello".translate(table) # "h2ll4"
Join (the inverse of split)#
",".join(["a", "b", "c"]) # "a,b,c"
"".join(["H", "i"]) # "Hi"
"-".join(str(x) for x in nums)
# join requires str elements
",".join(map(str, [1, 2, 3]))
Validation methods#
"42".isdigit()
"42.5".isdigit() # False
"abc".isalpha()
"abc123".isalnum()
"ABC".isupper()
" ".isspace()
"0123".isdecimal()
"012b".isdecimal() # False — hex digit
String formatting — four generations#
1. f-strings (preferred, 3.6+)#
name, score = "Alice", 95.5
msg = f"{name} scored {score:.1f}%"
# Expressions allowed
f"{nums[0]=}" # "nums[0]=10" debug syntax (3.8+)
f"{len(items)}"
f"{x if x > 0 else 0}"
# Format spec mini-language
f"{n:10}" # width 10
f"{n:>10}" # right align
f"{n:<10}" # left align
f"{n:^10}" # center
f"{n:010d}" # zero-pad int
f"{n:.2f}" # 2 decimal places
f"{n:08b}" # binary, width 8, zero-pad
f"{n:,}" # thousands separator 1,000,000
f"{pct:.1%}" # percentage
2. str.format()#
"{} scored {}".format(name, score)
"{name} scored {score:.1f}".format(name=name, score=score)
"{0} {1} {0}".format("a", "b") # positional
"{name}".format_map({"name": "Alice"})
3. % interpolation (legacy)#
4. Template strings (safe substitution)#
Interview default: f-strings unless working with legacy code.
Comparisons and sorting#
"apple" < "banana" # True — lexicographic Unicode order
"10" < "2" # True — string comparison, not numeric!
"Z" < "a" # True — uppercase before lowercase in ASCII
sorted(["cat", "bat", "rat"])
sorted(words, key=len)
sorted(words, key=str.lower)
sorted(num_strings, key=int)
Locale-aware sorting requires locale.strxfrm or third-party libraries — not typical in interviews.
Encoding and decoding#
s = "café"
b = s.encode("utf-8") # bytes: b'caf\xc3\xa9'
s2 = b.decode("utf-8") # str: "café"
len(s) # 4 characters
len(b) # 5 bytes in UTF-8
# Errors handling
b"\xff".decode("utf-8") # UnicodeDecodeError
b"\xff".decode("utf-8", errors="replace") # '\ufffd'
b"\xff".decode("utf-8", errors="ignore") # ''
| Type | Use |
|---|---|
str |
Text, interview problems |
bytes |
Binary protocols, file I/O raw |
bytearray |
Mutable bytes buffer |
Never mix str and bytes without explicit encode/decode.
Regular expressions (re)#
When string methods aren't enough:
import re
re.match(r"^\d+", "42abc") # match at start
re.search(r"\d+", "abc42def") # first anywhere
re.findall(r"\d+", "a1b22") # ['1', '22']
re.sub(r"\s+", " ", text.strip()) # collapse whitespace
re.split(r",\s*", "a, b, c") # ['a', 'b', 'c']
# Groups
m = re.match(r"(\w+)@(\w+)", "user@host")
m.group(1), m.group(2)
# Compile for reuse
pattern = re.compile(r"\d+")
pattern.findall("a1b2")
Raw strings (r"...") avoid double-escaping backslashes in patterns.
Standard library helpers#
from collections import Counter
Counter("aabbc") # Counter({'a': 2, 'b': 2, 'c': 1})
Counter("aabbc").most_common(2) # [('a', 2), ('b', 2)]
"".join(sorted("cba")) # "abc" — anagram check
import itertools
"".join(itertools.islice(s, 0, 5)) # first 5 chars lazy
Interview patterns — multiple approaches#
Palindrome check#
# Approach 1: slice reverse (after cleaning)
def is_palindrome_slice(s: str) -> bool:
cleaned = "".join(ch.lower() for ch in s if ch.isalnum())
return cleaned == cleaned[::-1]
# Approach 2: two pointers (no extra string for in-place on array of chars)
def is_palindrome_two_pointer(s: str) -> bool:
lo, hi = 0, len(s) - 1
while lo < hi:
if s[lo] != s[hi]:
return False
lo += 1
hi -= 1
return True
# Approach 3: recursive
def is_palindrome_rec(s: str) -> bool:
if len(s) <= 1:
return True
return s[0] == s[-1] and is_palindrome_rec(s[1:-1])
Two pointers is best when skipping non-alphanumeric without building cleaned.
Anagram detection#
from collections import Counter
# Approach 1: Counter
def is_anagram(a: str, b: str) -> bool:
return Counter(a) == Counter(b)
# Approach 2: sort
def is_anagram_sort(a: str, b: str) -> bool:
return sorted(a) == sorted(b) # O(n log n)
# Approach 3: frequency array (26 letters)
def is_anagram_array(a: str, b: str) -> bool:
if len(a) != len(b):
return False
freq = [0] * 26
for ca, cb in zip(a, b):
freq[ord(ca) - ord("a")] += 1
freq[ord(cb) - ord("a")] -= 1
return all(f == 0 for f in freq)
Longest substring without repeating characters#
def length_of_longest_substring(s: str) -> int:
seen: dict[str, int] = {}
best = left = 0
for right, ch in enumerate(s):
if ch in seen and seen[ch] >= left:
left = seen[ch] + 1
seen[ch] = right
best = max(best, right - left + 1)
return best
Alternative: use set and shrink window while duplicate exists.
Valid parentheses / bracket matching#
def is_valid(s: str) -> bool:
stack = []
pairs = {")": "(", "]": "[", "}": "{"}
for ch in s:
if ch in "([{":
stack.append(ch)
elif ch in ")]}":
if not stack or stack[-1] != pairs[ch]:
return False
stack.pop()
return not stack
See Stack.
Interview traps (quick reference)#
| Trap | What goes wrong | Safe approach |
|---|---|---|
+= in loop |
O(n²) concatenation | "".join(parts) |
"10" < "2" |
Lexicographic sort | key=int for numeric strings |
| Mutating assumption | No item assignment on str | Build new string or list of chars |
.split() default |
Splits on any whitespace | Explicit sep when needed |
find vs index |
-1 vs exception |
Choose based on control flow |
| Unicode vs bytes | TypeError mixing |
Encode/decode explicitly |
| Empty string | s[0] raises |
Guard if not s: |
| Case sensitivity | missed matches | .lower() or .casefold() |
isdigit vs isdecimal |
Different Unicode sets | Pick correct validator |
Mental model checklist#
- Why is
"".join()preferred over+=in a loop? - What is the difference between
split("=", 1)andpartition("=")? - How do you reverse a string without a loop?
- What does
ord/chrgive you for lowercase letter indexing? - When would you use
bytesinstead ofstr?
What's next#
| Topic | Page |
|---|---|
| Collections for frequency maps | Data Structures |
| Loop patterns | Control Flow |
| String DSA patterns | String Matching |
| Built-in helpers | Basic Built-in Functions and Libraries |