DEV Community

Samuel Ochaba
Samuel Ochaba

Posted on

How Python Strings Actually Work

If you write this code:

name = "alice"
name.upper()
print(name)
Enter fullscreen mode Exit fullscreen mode

you will get "alice" and NOT "ALICE".

This is the first fundamental truth about Python strings: they're immutable.

What Immutability Means

When you call upper(), Python doesn't reach into memory and change the letters. It can't. String objects, once created, cannot be modified.

What upper() actually does is build an entirely new string and return it:

>>> s = "hello"
>>> result = s.upper()  # Creates new string "HELLO"
>>> print(s)            # Original unchanged
hello
>>> print(result)       # New string
HELLO
Enter fullscreen mode Exit fullscreen mode

The original string still exists, unchanged. The variable s still points to it. upper() handed us a completely different object.

To "modify" a string, you have to reassign:

name = name.upper()  # Now name points to the new string
Enter fullscreen mode Exit fullscreen mode

Why This Design?

It's not arbitrary. Immutability enables:

1. Dictionary Keys

Strings can be used as dict keys because Python guarantees they won't change after hashing.

2. Thread Safety

Multiple threads can read the same string without locks—neither can modify it.

3. Memory Optimization

Python can reuse identical strings (interning):

>>> a = "hello"
>>> b = "hello"
>>> a is b  # Same object!
True
Enter fullscreen mode Exit fullscreen mode

The Performance Trap

Immutability has a cost. Consider:

result = ""
for line in log_file:
    result += line
Enter fullscreen mode Exit fullscreen mode

Each +=:

  1. Creates a new string
  2. Copies all existing characters
  3. Appends the new line
  4. Discards the old string

For 10,000 lines: 10,000 objects, ~50 million character copies.

Time complexity: O(n²)—doubling input quadruples runtime.

The Solution

Collect pieces in a list. Join at the end:

parts = []
for line in log_file:
    parts.append(line)
result = "\n".join(parts)
Enter fullscreen mode Exit fullscreen mode

join() knows the final size, allocates once, and copies each piece exactly once.

Time complexity: O(n)

For 10,000 lines:

  • Concatenation: ~500ms
  • join(): ~1ms

500x faster.

The Pattern

This applies to:

  • Log aggregation
  • CSV/JSON generation
  • Building LLM prompts
  • Any assembly of text from parts

Whenever you reach for += in a loop, stop. Use a list and join().

Key Takeaways

  • String methods don't modify—they return new strings
  • Reassign to "change": s = s.upper()
  • Immutability enables hashing, thread safety, memory optimization
  • Never concatenate strings in loops—use join()

𝘍𝘳𝘰𝘮 𝘮𝘺 𝘶𝘱𝘤𝘰𝘮𝘪𝘯𝘨 𝘣𝘰𝘰𝘬 "𝘡𝘦𝘳𝘰 𝘵𝘰 𝘈𝘐 𝘌𝘯𝘨𝘪𝘯𝘦𝘦𝘳: 𝘗𝘺𝘵𝘩𝘰𝘯 𝘍𝘰𝘶𝘯𝘥𝘢𝘵𝘪𝘰𝘯𝘴." 𝘚𝘶𝘣𝘴𝘤𝘳𝘪𝘣𝘦 𝘵𝘰 𝘮𝘺 𝘚𝘶𝘣𝘴𝘵𝘢𝘤𝘬 𝘧𝘰𝘳 𝘤𝘩𝘢𝘱𝘵𝘦𝘳𝘴 𝘭𝘪𝘬𝘦 𝘵𝘩𝘪𝘴 𝘪𝘯 𝘺𝘰𝘶𝘳 𝘪𝘯𝘣𝘰𝘹.

https://substack.com/@samuelochaba?utm_campaign=profile&utm_medium=profile-page


Top comments (0)