Exercises - immutability patterns
Exercises — Immutability Patterns
Section titled “Exercises — Immutability Patterns”NamedTuple Exercises
Section titled “NamedTuple Exercises”Exercise 1 — 🟢 Beginner
Create an immutable Rectangle named tuple with width and height fields. Add methods that return new rectangles with scaled dimensions.
# expected:# r1 = Rectangle(4, 6)# r2 = r1.scale(2)# r3 = r1.scale(0.5)
# r1.area() → 24# r2.area() → 96 ← new rectangle, r1 unchanged# r3.area() → 6
# r1 → Rectangle(width=4, height=6) ← untouched# r2 → Rectangle(width=8, height=12)# r3 → Rectangle(width=2.0, height=3.0)Exercise 2 — 🟢 Beginner
Create an immutable Money named tuple with amount and currency fields. Add add and subtract methods that return new Money instances. Raise a ValueError if currencies don’t match.
# expected:# m1 = Money(100, "EUR")# m2 = Money(50, "EUR")# m3 = Money(30, "USD")
# m1.add(m2) → Money(amount=150, currency='EUR')# m1.subtract(m2) → Money(amount=50, currency='EUR')# m1.add(m3) → ❌ ValueError: currency mismatch EUR vs USD# m1 → Money(amount=100, currency='EUR') ← untouchedExercise 3 — 🟡 Intermediate
Create an immutable Vector3D named tuple with x, y, z fields. Add add, scale, and dot product methods, all returning new instances where applicable.
# expected:# v1 = Vector3D(1, 2, 3)# v2 = Vector3D(4, 5, 6)
# v1.add(v2) → Vector3D(x=5, y=7, z=9)# v1.scale(2) → Vector3D(x=2, y=4, z=6)# v1.dot(v2) → 32 (1*4 + 2*5 + 3*6)# v1 → Vector3D(x=1, y=2, z=3) ← untouchedFrozen Dataclass Exercises
Section titled “Frozen Dataclass Exercises”Exercise 4 — 🟢 Beginner
Create a frozen User dataclass with name, email, and active fields. Add a deactivate and rename method that return new instances.
# expected:# user = User("Alice", "alice@example.com", active=True)
# user.deactivate() → User(name='Alice', email='alice@example.com', active=False)# user.rename("Alicia") → User(name='Alicia', email='alice@example.com', active=True)# user → User(name='Alice', ..., active=True) ← untouched
# Attempting mutation:# user.name = "Bob" → ❌ FrozenInstanceErrorExercise 5 — 🟡 Intermediate
Create a frozen HttpRequest dataclass with method, url, headers (as a tuple of tuples), and body fields. Add with_header and with_body methods that return new instances.
# expected:# req = HttpRequest("GET", "https://api.example.com/users", headers=(), body=None)
# req.with_header("Authorization", "Bearer token123")# → HttpRequest(method='GET', url='...', headers=(('Authorization', 'Bearer token123'),), body=None)
# req.with_body('{"name": "Alice"}')# → HttpRequest(method='GET', url='...', headers=(), body='{"name": "Alice"}')
# req ← untouchedExercise 6 — 🟡 Intermediate
Create a frozen Pipeline dataclass that holds a tuple of processing steps. Add an add_step method that returns a new Pipeline with the step appended, then execute the pipeline on an input value.
# expected:# p1 = Pipeline(steps=())# p2 = p1.add_step(str.strip)# p3 = p2.add_step(str.lower)# p4 = p3.add_step(str.title)
# p4.run(" HELLO WORLD ") → "Hello World"# p1 → Pipeline(steps=()) ← untouchedImmutable Dict Exercises
Section titled “Immutable Dict Exercises”Exercise 7 — 🟢 Beginner
Given a user dict, produce three new dicts — one with an updated email, one with a new field added, and one with the age field removed — without modifying the original.
user = {"name": "Alice", "email": "alice@old.com", "age": 30}
# expected:# updated → {"name": "Alice", "email": "alice@new.com", "age": 30}# extended → {"name": "Alice", "email": "alice@old.com", "age": 30, "role": "admin"}# reduced → {"name": "Alice", "email": "alice@old.com"}
# user → {"name": "Alice", "email": "alice@old.com", "age": 30} ← untouchedExercise 8 — 🟡 Intermediate
Write a function update_nested that updates a value in a nested dict without mutating any of the original dicts.
config = { "database": { "host": "localhost", "port": 5432, }, "cache": { "host": "localhost", "port": 6379, }}
# expected:# update_nested(config, "database", "port", 9999)# → {"database": {"host": "localhost", "port": 9999}, "cache": {...}}
# config["database"]["port"] → 5432 ← original untouchedExercise 9 — 🔴 Advanced
Write a function deep_merge that merges two nested dicts immutably — nested keys are merged recursively, and the originals are never mutated.
defaults = { "server": {"host": "localhost", "port": 8080, "debug": False}, "database": {"host": "localhost", "port": 5432},}
overrides = { "server": {"port": 3000, "debug": True}, "logging": {"level": "INFO"},}
# expected:# deep_merge(defaults, overrides) → {# "server": {"host": "localhost", "port": 3000, "debug": True},# "database": {"host": "localhost", "port": 5432},# "logging": {"level": "INFO"},# }
# defaults ← untouched# overrides ← untouchedCombined Exercises
Section titled “Combined Exercises”Exercise 10 — 🟡 Intermediate
Create an immutable ShoppingCart using a frozen dataclass that holds a tuple of items. Add add_item, remove_item, and total methods — all returning new instances where applicable.
# expected:# cart = ShoppingCart(items=())# cart1 = cart.add_item({"name": "apple", "price": 1.5})# cart2 = cart1.add_item({"name": "banana", "price": 0.5})# cart3 = cart2.add_item({"name": "cherry", "price": 3.0})
# cart3.total() → 5.0# cart3.remove_item("banana") → ShoppingCart with apple and cherry only# cart3.total() → 5.0 ← cart3 untouched# cart → ShoppingCart(items=()) ← original untouchedExercise 11 — 🔴 Advanced
Create an immutable EventLog using a NamedTuple that records a sequence of events as a tuple. Add append, filter_by_type, and replay methods — replay applies all events in order to an initial state dict and returns the final state.
# expected:# log = EventLog(events=())# log1 = log.append({"type": "set", "key": "name", "value": "Alice"})# log2 = log1.append({"type": "set", "key": "score", "value": 0})# log3 = log2.append({"type": "set", "key": "score", "value": 42})# log4 = log3.append({"type": "delete","key": "name"})
# log4.replay({})# → {"score": 42} ← name was deleted, score was set twice
# log4.filter_by_type("set").replay({})# → {"name": "Alice", "score": 42} ← delete event excluded
# log → EventLog(events=()) ← original untouchedTry implementing every solution without ever calling .append(), update(), or any other mutating method on the original data — if you find yourself modifying in place, step back and return a new instance instead.