Why does calling s.upper() on a Python string leave s unchanged?
answer
- The object never changes, only the name
- Methods answer questions, they do not command
- No item assignment on this type
- Assign the result or lose it
- Stable value is what makes it hashable
basics
~20 sPython str objects are immutable, so no method can change one in place. str.upper() builds and returns a brand-new string and leaves the original untouched, which is why you must assign the result: s = s.upper().
solid answer
~40 sA `str` is a fixed sequence of code points; once built it can never be altered. That is why `str` has no mutating API at all — `upper()`, `strip()`, `replace()` and slicing each allocate and return a **new** string, and `s[0] = 'A'` raises `TypeError: 'str' object does not support item assignment`. The usual beginner bug is calling `s.strip()` on a line by itself and expecting `s` to change; you have to rebind with `s = s.strip()`. Rebinding is not mutation: any other name still pointing at the old string keeps seeing the old value, unlike a `list`, where `append` is visible through every alias. Immutability is what makes strings hashable and therefore usable as `dict` keys and `set` members, and safe to share without defensive copies.
code
python · 13 liness = "audit"
print(s.upper()) # AUDIT
print(s) # audit -- unchanged
print(s.replace("a", "A")) # Audit
print(s) # audit -- still unchanged
s = s.upper() # rebinding is how you keep the result
print(s) # AUDIT
try:
s[0] = "a"
except TypeError as exc:
print(type(exc).__name__, exc)go deeper
Recall the one-liner: strings are immutable, so every method returns a new string and you must assign it. Be able to say what s[0] = 'A' raises and to spot a discarded s.strip() in review.
Explain the mechanics: rebinding a name versus mutating an object, why aliases of a string never see a change while aliases of a list do, and why += on a str is an assignment rather than an edit.
Show you have felt the cost. Point out where repeated transformations on large text allocate full copies, and name the accumulation tools — a list plus join, io.StringIO, bytearray for octets — you switch to in hot paths.
Own the design argument: immutability buys hashability, lock-free sharing across threads and constant folding, at the price of copying on every transform. Be ready to say when a codebase should standardise on builders instead of ad-hoc concatenation.
## What immutability actually means here An immutable object is one whose value is fixed for its whole lifetime. A Python `str` holds a sequence of Unicode code points that is decided when the object is created and can never afterwards be edited. This is a property of the *object*, not of the *name* that refers to it — you can freely point the name `s` at a different string, and beginners often mistake that rebinding for mutation. The consequence is visible in the type's API: `str` offers nothing that changes a string. There is no `append`, no `insert`, no in-place `sort`. Item assignment fails because `str` implements no assignment hook for subscripts, so `s[0] = 'A'` raises `TypeError: 'str' object does not support item assignment`, and `del s[0]` fails for the same reason. ## Every method returns a new object Each method that looks like a transformation is really a constructor. `upper()`, `lower()`, `casefold()`, `strip()`, `replace()`, `center()`, `zfill()`, `expandtabs()`, `title()` — all of them allocate a fresh string and hand it back. So does slicing: `s[1:]` is a new object, not a window onto the old one. Even `encode()` follows the rule, returning a new `bytes` object rather than converting anything in place. This produces the single most common early Python bug: ```python name = " ada " name.strip() # result computed, then thrown away print(repr(name)) # ' ada ' — nothing happened name = name.strip() # the fix: rebind ``` A linter will usually flag the discarded expression, but the mental model matters more than the tool: a `str` method call is a *question*, not a *command*. If you do not keep the answer, you have done nothing but burn a little CPU. ## Rebinding versus mutating The difference between rebinding and mutating shows up as soon as two names share an object: ```python a = "log" b = a a = a.upper() # a is rebound to a new object; b still sees the old one # a == 'LOG', b == 'log' xs = ["log"] ys = xs xs.append("line") # mutation: ys sees it too # ys == ['log', 'line'] ``` That is also why `s += 'x'` does not disprove immutability. Augmented assignment on a `str` computes a new value and stores it back under the same name; the old string object is simply left unreferenced and freed. The name changed, the object did not. ## Why the language chose this Three reasons come up in interviews. **Hashability.** A hash-based container places a key in a bucket derived from its hash value. If the key could change after insertion its hash would change and it would be unfindable. Because a `str` can never change, it has a stable hash (CPython even caches it on the object), so strings are the default key type for dictionaries, the members of sets, and legal in any position that demands hashability. **Safe sharing.** An object nobody can modify can be passed anywhere, stored in any structure, captured by any closure and read from any thread without a defensive copy and without aliasing bugs. Half the reason strings are pleasant to work with in Python is that you never have to ask who else holds a reference. **Optimization headroom.** Because two equal strings are interchangeable, the compiler can fold literal expressions at compile time and the runtime can share a single object for identical literals and identifier-like strings. None of that is safe for a mutable type. ## The cost you accept in exchange Immutability is not free: every transformation copies the entire string. One `replace()` on a large document allocates a second document. Chain four transformations and you have allocated four full copies, of which three are immediately garbage. For one-off work that is irrelevant; inside a loop it is the origin of the classic quadratic-concatenation problem, and it is why Python gives you dedicated accumulation tools instead. ## What to use when you genuinely need to edit When you must build or revise text incrementally, do not fight the type — swap it out. Collect the pieces in a `list` and call `"".join(pieces)` once at the end; write through an `io.StringIO` when the code is shaped like a series of writes; and when the data is octets rather than text, use a `bytearray`, which is the mutable sibling of the equally-immutable `bytes`. For a single edit, rebuilding from slices is perfectly idiomatic: `s = s[:i] + ch + s[i + 1:]`. In an interview, the answer that lands is the two-part one: strings are immutable so every method returns a new object, and therefore you assign the result — followed by naming the accumulation tools you reach for when copying would be wasteful.
- If Python strings are immutable, why does `s += 'x'` appear to work?Because augmented assignment on a `str` is not mutation. It computes a new string of the combined value and stores it back under the same name; the previous object is left unreferenced. Any other name still bound to the old string continues to see the old value, which is the giveaway that nothing was edited in place.
- Why does a Python string's immutability matter for using it as a dict key?A hash container puts a key in a bucket chosen from its hash. If the key's value could change after insertion, its hash would change and the entry would become unfindable. Because a `str` can never change, its hash is stable — CPython caches it on the object — so strings are the natural key type for `dict` and member type for `set`.
- Which built-in gives you a mutable stand-in when you really must edit text?There is no mutable `str`. Use a `list` of pieces and `"".join(...)` once at the end, or an `io.StringIO` when the code is a sequence of `write()` calls. If the data is octets rather than text, `bytearray` is the mutable counterpart of `bytes`. Convert back once, at the end.
A string is a printed page, not a whiteboard. upper() does not rewrite the page; it prints you a new one in capitals and hands it over. If you throw that new page away, the original is still exactly as it was.
saying these in an interview costs you the question
- Believes str.upper() edits the string in place
- Calls s.strip() on its own line and never assigns it
- Expects s[0] = 'A' to work on a str
- Argues += proves Python strings are mutable
- Thinks immutability means the name cannot be rebound
- Assumes slicing a str returns a view, not a copy