Skip to content

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)

  1. What a Python string actually is
  2. Creating strings — all forms
  3. Indexing and slicing
  4. Immutability in depth
  5. Character and Unicode operations
  6. String methods — comprehensive reference
  7. String formatting — four generations
  8. Comparisons and sorting
  9. Encoding and decoding
  10. Regular expressions (re)
  11. Standard library helpers
  12. 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)
s = "café"
len(s)        # 4 characters (not bytes)
type(s)       # <class 'str'>

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

# SLOW: each += allocates new string
result = ""
for ch in chars:
    result += ch

# FAST: O(n)
result = "".join(chars)

# Also good for list of strings
result = "".join(buffer)

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:

""[0]       # IndexError
""[0:1]     # "" — no error
"abc"[10]   # IndexError
"abc"[1:100]  # "bc"

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 tuple
  • split() — 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)#

"%s scored %.1f" % (name, score)
"%(name)s scored %(score).1f" % {"name": name, "score": score}

4. Template strings (safe substitution)#

from string import Template
Template("$name scored $score").substitute(name="Alice", score=95)

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#

  1. Why is "".join() preferred over += in a loop?
  2. What is the difference between split("=", 1) and partition("=")?
  3. How do you reverse a string without a loop?
  4. What does ord/chr give you for lowercase letter indexing?
  5. When would you use bytes instead of str?

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