Recipe Manager
Permissions and immutable revision history
The problem
A recipe manager stores structured content that people can edit and revisit. Its central rules are easy to state: current recipe names must be unique, readers must have permission, and an edit must not rewrite an earlier revision. Designing the class means deciding who owns those rules and what an unsuccessful operation leaves behind. Ingredients and steps are ordered lists; they are content, rather than mathematical sets, so duplicates remain meaningful.
Separate identity from presentation. A recipe keeps the same identifier when its name changes. A version number identifies one snapshot within that recipe. A user identifier identifies a principal with explicit capabilities. Treating these as interchangeable strings inside a single list makes later operations difficult to reason about. The manager coordinates authorization, current names, and revision creation; a revision only represents content at one point in the history.
Naive approach
Start with a list of dictionaries. Search the list for an identifier or duplicate name, then change the matching dictionary in place. Keep a second list of users and scan it before an edit. This is a reasonable way to learn the operations, and a small catalog may never need a more elaborate search index. A simple scan is also useful for listing recipes that contain an ingredient.
A shallow snapshot still shares nested content:
current = {"name": "Soup", "steps": ["simmer"]}
history = [current.copy()]
current["steps"].append("serve")
print(history[0]["steps"])
assert history[0]["steps"] == ["simmer", "serve"]The earlier revision changed without a revision operation. Copying the outer record protected its keys, but not the mutable list those keys reference.
For undo, copy the previous dictionary into a history list. However, copying the outer dictionary alone still shares its ingredient and step lists. Mutating one of those lists can silently change a supposedly historical recipe.
Where the naive approach breaks
In-place editing erases evidence of what readers previously saw. Returning internal lists lets a caller bypass authorization and version creation entirely. Renaming before checking uniqueness can leave both the name lookup and content in an inconsistent state when the operation fails.
Discarding all versions after an undo also loses the audit trail. Python list slicing copies references into a new list; retaining k entries costs O(k), rather than O(1). More fundamentally, a destructive undo is a different product contract from restoring old content while preserving every revision. Decide which behavior is required before choosing the data structure.
A better design
Use maps for users and recipes, a name-to-identifier map for current uniqueness, and an append-only list of snapshots per recipe. Store content as immutable tuples internally or make defensive copies at every boundary. Check the required capability and all failure conditions before appending a version or changing the name index. A write capability does not automatically grant read access in this exercise.
Represent retained content with immutable fields:
from dataclasses import dataclass
@dataclass(frozen=True)
class Revision:
name: str
steps: tuple[str, ...]
history = [Revision("Soup", ("simmer",))]
history.append(Revision("Tomato Soup", ("simmer", "serve")))
history.append(history[0]) # Restore content by appending a new position.
print([revision.name for revision in history])
assert len(history) == 3Sharing an immutable revision value is safe; its list position supplies the new version number here. The manager still checks authorization and current-name uniqueness before appending. Frozen records containing mutable lists would not provide the same protection.
Hash lookups and name checks take expected O(1) time, excluding content copying. Creating a revision takes O(I + S) for I ingredients and S steps. Direct version selection is O(1), but returning its snapshot still copies its content. Ingredient search scans current recipes and sorts matching results; an inverted index could help frequent searches, at the cost of extra updates on every edit and restore.
Real-world usage
The same responsibilities appear in document editors, configuration catalogs, and editorial tools. Stable identity allows references to survive renames; immutable revisions support review and recovery. This small manager has local permission records and sequential calls. Durable audit storage, identity verification, concurrent edits, and retention policies would require additional design. A version number alone does not prevent two writers from overwriting each other's intended changes; a larger API might require an expected current version.
Put it into practice
Implement the exercise in a full-screen workspace, then run its tests.