Strings

Immutable sequences with a rich method API — the most-used type in real Python code

Why

String manipulation is everywhere in real code. Python's string methods are expressive enough that one call often replaces a loop — knowing join, split, and find vs index pays off constantly. f-strings are the standard for any output formatting.

Intuition

Think of a string as a frozen list of characters. You can read any character by index, slice a range out, or iterate over it — but you can never change a character in place. Every method that looks like it modifies the string actually hands you back a brand-new one.

Explanation
1. Slicing & Core Methods

Strings support the same slice syntax as lists. Core methods cover the most common operations — searching, transforming, and cleaning.

s = "hello world" s[0] # "h" s[-1] # "d" s[0:5] # "hello" "ello" in s # True s.upper() # "HELLO WORLD" s.strip() # removes leading/trailing whitespace s.find("world") # 6 — returns -1 if not found s.index("world") # 6 — raises ValueError if not found s.startswith("hello") # True s.replace("world", "Python") # "hello Python" s.count("l") # 3

find returns -1 if the substring isn't found; index raises ValueError. Use in for a boolean check, find for a safe position lookup.

2. Step Slicing

The third argument in s[start:stop:step] controls the stride and direction — a negative step reverses traversal.

s = "abcde" # s[start:stop:step] — step controls direction and stride s[:] # "abcde" — omit start & stop = full string s[::2] # "ace" — every 2nd character (0, 2, 4) s[1::2] # "bd" — every 2nd starting at index 1 # step -1 reverses direction: start defaults to end, stop defaults to beginning s[::-1] # "edcba" — full reverse s[4:1:-1] # "edc" — from index 4 down to (not including) 1

Omitting start and stop always means "full range" — direction is set by the sign of step. [::-1] is the idiomatic reverse.

3. split, join & Formatting

split and join are inverses — split breaks a string into a list, join reassembles it. f-strings are the modern way to embed values inline.

# split and join are inverse operations "a,b,c".split(",") # ["a", "b", "c"] "hello world".split() # ["hello", "world"] — any whitespace ", ".join(["Alice", "Bob"]) # "Alice, Bob" "".join(["h","e","l","l","o"]) # "hello" # f-strings (preferred) name, score = "Alice", 98.5 f"{name}: {score:.1f}" # "Alice: 98.5" f"{42:05d}" # "00042" — zero-padded f"{1_000_000:,}" # "1,000,000" — thousands separator

"sep".join(items) — the separator goes on the left, not the right.

4. Why += in a Loop is O(n²)

Strings are immutable, so every += allocates a brand-new object and copies all previous characters. Concatenating n strings in a loop costs O(n²) — join avoids this by allocating once.

# strings are immutable — += doesn't append, it creates a new object s = "" s += "a" # creates new string "a", discards "" s += "b" # creates new string "ab", discards "a" s += "c" # creates new string "abc", discards "ab" # each step copies all previous characters → O(1+2+3+…+n) = O(n²) # bad — O(n²) result = "" for word in parts: result += word # good — O(n), join allocates once then writes each part in result = "".join(parts)

Every += on a string allocates a brand-new object and copies all existing characters into it. With join, Python measures the total length first, allocates once, and fills it — one pass, no copies.

5. Common Patterns

Quick one-liners for the most common string checks.

s = "racecar" s == s[::-1] # palindrome check # anagram — Counter is O(n), sorted is O(n log n) from collections import Counter Counter("listen") == Counter("silent") # True len(s) == len(set(s)) # all unique characters? # encode / decode — str ↔ bytes "café".encode("utf-8") # b'caf\xc3\xa9' b"hello".decode("utf-8") # "hello"

Four common string checks: palindrome (reversal equality), anagram (character frequency), unique characters (set size), and encoding/decoding between str and bytes.