Why are Python strings immutable?

Because a string's value can never change, its hash can never go stale and a reference to it can never be invalidated by someone else. That is what makes strings usable as dictionary keys and safe to share without copying. The cost is that every "modification" builds a new object.

Overview

The fact, and the four reasons

A Python string cannot be modified. Every operation that appears to change one produces a new object.

1Python
Output

Four design reasons, and each has practical consequences.

1. Hashability. A dictionary key's slot depends on its hash. If a string could change, its hash would change, and any dictionary entry keyed on it would become unreachable. Immutability is what makes strings usable as keys — and since attribute names, module names and keyword arguments are all dictionary keys, this is foundational rather than incidental.

2. Safe sharing. Passing a string to a function cannot mutate the caller's copy, so no defensive copying is needed anywhere. Two names can point at the same string with no risk.

3. Interning and caching. Identical short strings can share one object, saving memory. The interpreter also caches a string's hash after first computing it, which is only valid because the value cannot change.

4. Thread safety. An immutable object needs no lock. Any number of threads can read a string concurrently with no coordination.

StringsConceptualEasy

Step through it

What to watch

  • t = s makes no copy — both names point at one object.
  • upper() returns a different object; s is untouched.
  • The stable hash at the end is the payoff, not a side effect.

Say this out loud

"Immutable means every operation returns a new string. It buys hashability - so strings can be dict keys - and it means sharing a string is free. It costs you concatenation in a loop, which is why you use join."

Why are Python strings immutable?

Why are Python strings immutable, and what does that buy you?

Run it

Object identities before and after a "modification", then the cost of ignoring what that implies — measured at three sizes so the shape of the curve is visible rather than asserted.

2Python
Output

The performance consequence

The one real cost is building strings incrementally:

3Python
Output

At 10,000 words the first version performs roughly 50 million character copies. join computes the total length, allocates once, and copies each piece in.

This is the most common Python performance mistake, and the reason it survives review is that the quadratic version is fine on ten words and catastrophic on ten thousand.

Two related points:

CPython has an optimisation that sometimes makes += in a loop appear linear: if a string has exactly one reference, it can be resized in place. It is an implementation detail, it fails as soon as anything else holds a reference, and it does not exist in other Python implementations. Do not rely on it.

io.StringIO is the alternative when the pieces are produced incrementally and cannot be collected in a list first — it behaves like a writable buffer.

What to use instead when mutation is needed

NeedUse
Build a string from piecesA list, then "".join()
Incremental writesio.StringIO
Mutable bytesbytearray
Character-level editingA list of characters, join at the end
Templatingf-strings or str.format
Repeated replacementstr.translate with a mapping table

bytearray is the genuinely mutable relative:

4Python
Output

Note the pairing: str is immutable and bytes is immutable, while bytearray is the mutable byte sequence. There is no mutable str in Python, and a list of one-character strings is the usual stand-in.

What immutability actually means

There is no operation in Python that changes a string in place. s.upper(), s.replace(), s.strip() and s + t all return a new string and leave the original exactly as it was. Assigning to an index — s[0] = 'A' — is not slow, it is a TypeError.

This is enforced, not advisory. There is no private method and no escape hatch, which is what lets the interpreter make assumptions it could not otherwise make.

The three things it buys

Hashability. A dictionary stores a key in a slot chosen from its hash. If the key could change after insertion, its hash would no longer match its slot and the entry would become unreachable. Immutable types can be keys; mutable ones cannot, which is why {[1,2]: 'x'} raises and {(1,2): 'x'} does not.

Free sharing. Passing a string to a function, storing it in two places, closing over it — none of these need a defensive copy, because no one can modify it behind your back. In a language with mutable strings, library code often copies on the way in just in case.

Interning. CPython reuses one object for short string literals that look like identifiers, so equality can often be settled by an identity check. That is an optimisation immutability makes legal.

What it costs, and the one place it bites

Every edit allocates. Usually that is irrelevant — one replace on one line costs nothing you can measure. It matters in exactly one shape: building a string a piece at a time in a loop.

Each += copies everything accumulated so far, so n appends copy 1 + 2 + 3 + ... + n characters, which is O(n²). The fix is "".join(parts): one pass to total the lengths, one allocation, one copy. The editor below measures both.

The follow-up you should expect

"If strings are immutable, why does s += 'x' sometimes look fast?" CPython has a special case that resizes a string in place when the target is a plain local variable and nothing else refers to it. It is real, it is invisible, and it stops firing the moment you store into an attribute or a list. Do not build on it.

Interning and the is trap

Because strings are immutable, the interpreter may share one object for equal strings. That makes is behave unpredictably for strings:

5Python
Output

Short identifier-like literals are interned automatically at compile time. Strings built at runtime generally are not.

The rule that follows: use == for string comparison, always. is asks whether two names refer to the same object, which is almost never the question. Relying on interning produces code that works in testing and fails on computed input.

sys.intern() forces interning explicitly, which is occasionally worth it when the same strings are compared millions of times — identity comparison is faster than character comparison. That is a genuine optimisation in parsers and interpreters, and rare in application code.

Immutability elsewhere in Python

TypeMutableHashable
strNoYes
bytesNoYes
bytearrayYesNo
tupleNoYes, if its contents are
frozensetNoYes
listYesNo
dictYesNo
setYesNo

The pattern is consistent: mutable types are unhashable, for exactly the same reason strings must be immutable to be keys.

The tuple row has a subtlety worth knowing: a tuple is immutable, and if it contains a list it is unhashable, because its hash would depend on something that can change.

6Python
Output

How other languages compare

Java strings are immutable, for the same reasons, with StringBuilder as the mutable builder — the direct analogue of using a list and joining.

C++ std::string is mutable, so in-place modification and reversal are possible — which is why "reverse a string in place" is a C++ question that does not translate to Python.

JavaScript strings are immutable, and it has no separate builder type; array join is the idiom.

Rust distinguishes &str (an immutable borrowed slice) from String (an owned, growable buffer) in the type system.

The convergence is informative: most modern languages chose immutable strings, because the hashing, sharing and thread-safety benefits outweigh the cost of a separate builder for the one case where mutation is wanted.

Questions people ask

Does s += x create a new string? Yes, in general. CPython sometimes resizes in place when there is exactly one reference, and that is an implementation detail to ignore.

Why can strings be dictionary keys but lists cannot? Keys must have a stable hash, which requires immutability.

How do I modify a string then? Build a new one — slicing and concatenation, or a list of characters joined at the end.

Is bytearray the mutable string? It is the mutable bytes type. There is no mutable str.

Why is a is b sometimes True for equal strings? Interning shares identical literals. Never depend on it; use ==.

Does immutability cost memory? Sometimes more allocations, and sometimes less through sharing and interning. The predictability is usually worth more than either.

Recap in one screen

  • Strings cannot be modified; every "change" produces a new object.
  • Immutability is what makes them hashable, safely shareable, internable and thread-safe.
  • The cost is that += in a loop is O(n²) — use a list and "".join().
  • bytearray is the mutable byte sequence; there is no mutable str.
  • Interning makes is unreliable for strings; compare with ==.

How the code works

Object identities before and after a "modification", then the cost of ignoring what that implies — measured at three sizes so the shape of the curve is visible rather than asserted.

How the code works

  1. t = sBinds a second name to the same object, which t is s confirms. No copy is made because none is needed — neither name can change the value.
  2. u = s.upper()A different id. Every string method returns a new object; none of them has a way to modify the receiver.
  3. hash("cat") == hash(s)The hash is a function of the value, and the value is frozen for the object's whole life. That is the precondition a dictionary key has to meet.
  4. self.text += "x"Accumulating into an attribute rather than a local is deliberate: CPython can resize a local string in place, which would hide the quadratic the table is there to show.

Change one thing

  • Add v = "cat" and check v is s. Short literals are interned, so it is often True — and relying on that is still a bug.
  • Swap b.text for a local variable and re-run. Whether the quadratic disappears depends on your interpreter, which is itself the argument for join.

Where this runs

Real CPython, compiled to WebAssembly and running on your own machine — nothing is uploaded. The first run takes a few seconds while the interpreter downloads; after that it is immediate. Need more room, or want to paste your own attempt? Use the Python compiler.

Check yourself

0 of 3

Answer without scrolling back up.

  1. Why can a string be a dictionary key when a list cannot?

  2. What does s.upper() do to s?

  3. Building a string with += in a loop is O(n²) because each step:

Cheat sheet

Why are Python strings immutable?

Because a string's value can never change, its hash can never go stale and a reference to it can never be invalidated by someone else. That is what makes strings usable as dictionary keys and safe to share without copying. The cost is that every "modification" builds a new object.

INTERVIEW · vizlearn.in/interview/why-are-python-strings-immutable.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.