Input preservation is easier to reason about when data boundaries are explicit. A function can return a changed value while keeping the old value usable, but Python will not stop it from mutating shared objects. Treating an ordinary dictionary as read-only is a convention, documented by the API and checked on exercised cases. It differs from choosing a representation whose entire reachable contents support no ordinary mutation.
A tuple fixes its element references; it does not freeze the objects they reference. If a tuple contains a list, another owner can append to that list and the tuple will still point to the changed list. An outer mapping copy has a similar limit: it creates a new dictionary while retaining references to nested values.
record = {"name": "oak", "tags": ["tree"]}
shallow = dict(record)
shallow["tags"].append("green")
print(record["tags"])
assert shallow is not record
assert shallow["tags"] is record["tags"]That example intentionally demonstrates shared mutation. Copying only the top level would not fulfill a promise that later tag edits cannot change a returned snapshot. The lesson's schema makes the needed work precise: every record has one string name and a list or tuple of string tags. Convert tags to a tuple and create a new outer dictionary. Because the leaves are strings, there are no deeper mutable values to detach.
The build's with_tag strips the new tag, rejects an empty result, preserves existing order and duplicates, and appends only a genuinely new cleaned string. Equality is case-sensitive here. Normalization rules belong in the contract; functional style is no excuse to silently remove duplicates or rewrite existing tags.
old_tags = ("tree",)
clean_tag = " green ".strip()
new_tags = old_tags if clean_tag in old_tags else old_tags + (clean_tag,)
print(old_tags, new_tags)Reusing an immutable tuple is safe because neither owner can mutate its slots. An implementation need not allocate a redundant copy of an immutable object merely to look functional. Tuple concatenation returns a new value when a tag is added. The outer dictionary returned by with_tag is still mutable by its owner; its promise is independence from caller input, not a universal mutation barrier.
freeze_catalog goes further for this limited schema. It returns an outer tuple, pairs represented as tuples, tag collections represented as tuples, and string leaves. That whole representation is immutable under ordinary operations. This conclusion follows from every container and leaf type, not from the first outer tuple alone. General objects, custom classes, arbitrary graphs and hostile access are outside this transformation.
A deep copy would create independent mutable containers, which is useful for a different goal but does not make them immutable. Prefer transforming the small schema you actually own over inventing a universal freezer. In the exercises, draw aliases before predicting output. In the build, mutate the original list and mapping after return, test tuple inputs and empty catalogs, and verify that the returned dictionary can still be edited without changing the input.