Strings, and why they never change
Immutability and what it costs, slicing, f-strings, the join idiom, and the string operations coding rounds keep asking for, with complexity for each.
Strings are the most-used type in any coding round, and the one where a wrong instinct costs you the time limit rather than the answer.
A string cannot be modified
s = "hello"
s[0] = "H" # TypeError: 'str' object does not support item assignmentStrings are immutable. Every operation that looks like it changes a string actually builds a new one:
s = "hello"
t = s.upper()
print(s, t) # hello HELLO — s is untoucheds.upper() did not modify s; it returned a new string and s still points at the old one.
This catches people out constantly:
name = " raja "
name.strip() # returns "raja" and throws it away
print(repr(name)) # ' raja '
name = name.strip() # reassign — this is the fix
print(repr(name)) # 'raja'Every string method returns a new string. If you are not assigning the result, you are doing nothing.
What immutability costs you
Building a string in a loop with += is quadratic:
# O(n²) — each += copies the whole string built so far
out = ""
for word in words:
out += word
# O(n) — build a list, join once
parts = []
for word in words:
parts.append(word)
out = "".join(parts)For ten words the difference is invisible. For a hundred thousand it is the difference between
passing and timing out. join is the idiom; use it.
join also reads better than it looks at first. The separator is the string you call it on:
print(", ".join(["a", "b", "c"])) # a, b, c
print("".join(["a", "b", "c"])) # abc
print("\n".join(lines)) # one per lineOne rule: join needs strings. A list of integers raises TypeError, so convert first:
print(",".join(str(n) for n in [1, 2, 3])) # 1,2,3Slicing
s[start:stop:step] — start included, stop excluded, and every index optional.
s = "placement"
print(s[0]) # p
print(s[-1]) # t negative counts from the end
print(s[0:5]) # place
print(s[:5]) # place
print(s[5:]) # ment
print(s[::2]) # pae et
print(s[::-1]) # tnemecalp — reverse
print(s[:]) # placement — a full copys[::-1] is how you reverse a string in Python, and it is worth memorising because reversal
appears in palindrome questions constantly:
def is_palindrome(s):
clean = "".join(c.lower() for c in s if c.isalnum())
return clean == clean[::-1]
print(is_palindrome("A man, a plan, a canal: Panama")) # TrueSlicing never raises IndexError, even when the range is nonsense — unlike single indexing:
s = "abc"
print(s[10:20]) # '' — empty, no error
print(s[10]) # IndexErrorThat asymmetry is deliberate and occasionally useful, but it also means a slicing bug fails silently with an empty result rather than loudly with an exception.
f-strings
Since Python 3.6, formatting is done with an f prefix and expressions in braces:
name, score = "Raja", 87.456
print(f"{name} scored {score}") # Raja scored 87.456
print(f"{name} scored {score:.1f}") # Raja scored 87.5
print(f"{score:>10.2f}") # 87.46 right-aligned in 10 chars
print(f"{1234567:,}") # 1,234,567
print(f"{0.8734:.1%}") # 87.3%
print(f"{name=}, {score=}") # name='Raja', score=87.456That last one — f"{x=}" — prints the expression and its value, and is the fastest debugging
tool in the language.
You can put any expression inside the braces:
items = [1, 2, 3]
print(f"{len(items)} items, total {sum(items)}") # 3 items, total 6The older styles still work and you will meet them in existing code:
"%s scored %d" % ("Raja", 87) # C-style, oldest
"{} scored {}".format("Raja", 87) # .format(), Python 2.6+
f"{name} scored {score}" # f-string, prefer thisThe methods worth knowing cold
s = "Placement Season 2027"
s.lower(), s.upper() # case
s.strip(), s.lstrip(), s.rstrip() # whitespace at ends (or given characters)
s.split() # ['Placement', 'Season', '2027'] — splits on any whitespace
s.split("e") # split on a specific separator
s.replace("2027", "2028") # every occurrence
s.startswith("Place") # True
s.endswith("2027") # True
s.find("Season") # 10 — index, or -1 if absent
s.index("Season") # 10 — index, or ValueError if absent
s.count("e") # 4
s.zfill(25) # pad with leading zeros
s.title() # 'Placement Season 2027'The find / index pair is a real distinction: find returns -1 for absent, index raises.
Use find when absence is normal, index when absence is a bug.
Character tests, useful for cleaning input:
"a".isalpha() # True
"1".isdigit() # True
"a1".isalnum() # True
" ".isspace() # True
"Abc".islower() # FalseAnd in is the readable membership test — no method needed:
print("Season" in s) # TrueComplexity, so you can reason about the time limit
| Operation | Cost | Note |
|---|---|---|
s[i] |
O(1) | index |
len(s) |
O(1) | stored, not counted |
s + t |
O(n + m) | builds a new string |
s += t in a loop |
O(n²) | use join |
"".join(parts) |
O(total) | one pass |
s[a:b] |
O(b − a) | copies |
x in s |
O(n · m) | substring search |
s.replace(...) |
O(n) | new string |
s[::-1] |
O(n) | new string |
The two lines to remember: += in a loop is quadratic, and every slice is a copy. A
sliding-window solution that slices the window on each step is secretly O(n²).
What to take into an interview
- Strings are immutable; every method returns a new string, so assign the result.
- Building a string with
+=in a loop is O(n²)."".join(list)is O(n). s[::-1]reverses. Slicing out of range gives''; indexing out of range raises.- f-strings are the current formatting style.
f"{x=}"prints name and value. findreturns −1 when absent;indexraisesValueError.inis the membership test, and on a string it is a substring search.