← Home

010. Strings

Immutable sequences of Unicode characters

010. Strings

Aryan’s RAM monitor reads process names from ps aux. They arrive inconsistently: “Chrome”, “chrome”, “ CHROME “, “Google Chrome”.

He needs a normalizer so that the same process is always stored under the same key in his process table. If “Chrome” and “chrome” map to different keys, he ends up with duplicate entries and wrong memory totals.

The normalizer must:

  1. Strip leading/trailing whitespace (" chrome " → "chrome")
  2. Lowercase the name ("Chrome" → "chrome")
  3. Replace internal spaces with underscores ("google chrome" → "google_chrome")

The broken code does steps 1 and 3 but skips step 2 — so “Chrome” and “chrome” remain different keys in the process table.

This is the same bug Aryan hit with username normalization.


💡 Fun fact: Python strings are immutable — once created, their bytes cannot change. This is why every string method returns a new string rather than modifying the original. Immutability also makes strings safe to use as dictionary keys and allows Python to intern (cache) common strings as a performance optimisation.

⚠️ Watch out: The most common mistake is calling .strip(), .lower(), or .replace() without reassigning the result. Writing name.strip() alone on a line does nothing useful — you must write name = name.strip() to capture the new string.

🤔 Think about it: If strings are immutable, what actually happens in memory when you chain name.strip().lower().replace(" ", "_")? How many string objects are created?

Learning objectives

  • Use common string methods (strip, lower, replace)
  • Understand that string methods return new strings (immutable)
  • Chain string operations correctly

Key concepts

  • str
  • string methods
  • immutability

Try it

Concept detail

Python strings (str) are immutable sequences of Unicode characters. Immutable means once a string is created, its characters cannot be changed. Every string method returns a NEW string object; it does not modify the original.

s = “Hello” s.lower() # returns “hello” — s is still “Hello” s = s.lower() # now s points to the new string “hello”

This is why each step must reassign: name = name.strip() etc.

Common methods used here: strip() remove leading/trailing whitespace lower() convert all characters to lowercase upper() convert all characters to uppercase replace(old, new) replace all occurrences of old with new

The order of operations matters. If you replace spaces before lowercasing, the result is still correct here — but in general, normalise case first so you don’t need to think about whether your replacement strings need casing variants.

Strings also support indexing (s[0]), slicing (s[1:4]), len(), and in operator.

Solution

def format_username(name):
    name = name.strip()
    name = name.lower()
    name = name.replace(" ", "_")
    return name

Tests

def test_strips_whitespace():
    assert format_username("  alice  ") == "alice"

def test_lowercases():
    assert format_username("Alice") == "alice"

def test_replaces_spaces():
    assert format_username("John Doe") == "john_doe"

def test_combined():
    assert format_username("  Bob Smith  ") == "bob_smith"

def test_already_clean():
    assert format_username("charlie") == "charlie"

def test_uppercase_with_spaces():
    assert format_username("  GOOGLE CHROME  ") == "google_chrome"

Resources