Inventory model: hash/equality contract, property dispatch from __init__, shared mutable class state
An internal inventory package models products with a plain class. Product defines __eq__ comparing sku and was recently made a key in a dict cache; after deployment, 200 requests failed with TypeError: unhashable type: 'Product'. A hotfix added __hash__ = object.__hash__, and now two Product instances with the same sku are treated as different keys, so the cache grew to 1.2 million entries and stock counts diverged. The class also has @property price with a setter that validates 0 <= price <= 100000, and DiscountedProduct(Product) overrides the setter to read self._discount. Instantiating DiscountedProduct(sku="A-1", price=50, discount=0.1) raises AttributeError: 'DiscountedProduct' object has no attribute '_discount' because Product.__init__ assigns self.price before the subclass sets _discount. Finally, tags = [] is a class attribute, and all 50,000 instances share it. The team is considering a @dataclass rewrite.
1. Explain the unhashable error and why the hotfix is wrong. Give correct hash/equality designs for value semantics and for mutable entity semantics, including the dataclass option and the best cache key.
Read the worked answer
Defining __eq__ in a class sets __hash__ to None unless you explicitly define it, which is why Product became unhashable. The hotfix __hash__ = object.__hash__ restores hashability but violates the hash/equality contract: if a == b because they share a sku, they must have the same hash. Identity hashes differ, so equal products land in different dict buckets; lookups miss, the cache grows, and stock updates apply to duplicate keys.
Choose one semantic. If Product is a value object and sku is immutable, implement def __hash__(self): return hash(self.sku) and make sku read-only; or use @dataclass(frozen=True) with eq=True, which generates both methods consistently. If products are mutable entities, identity semantics may be correct: set eq=False, or leave object.__eq__, and key the cache by sku instead of by the object. The simplest production fix is often to key the cache with product.sku and avoid relying on object hashing at all. Do not use unsafe_hash=True on a mutable dataclass: mutate a hashed field after insertion and the dict entry becomes unreachable. For DiscountedProduct, if the parent is frozen, the subclass must also be frozen; dataclass inheritance does not let a non-frozen subclass inherit a frozen one. Add a test: equal objects have equal hashes, and a dict lookup after mutation behaves as documented.
2. Why does `DiscountedProduct(...)` fail in `Product.__init__`, and what are three possible repairs? Which repair would you choose, and how does `__post_init__` change the answer under a dataclass?
Read the worked answer
Product.__init__ does self.price = price, which invokes the property setter found on type(self). For a DiscountedProduct, that is the subclass setter, and it reads self._discount before DiscountedProduct.__init__ has assigned it. This is the classic calling-an-overridable-method-from-the-base-constructor problem; the property is an overridable method.
Three repairs, in order of robustness: (1) Do not call overridable properties from __init__. Have Product.__init__ assign the backing field (self._price = price) and expose validation through a private, non-overridden helper or a separate validate() step. (2) In the subclass, set _discount before calling super().__init__(). That works but is fragile and undocumented. (3) Make the setter tolerant: discount = getattr(self, '_discount', 0). That hides ordering bugs and is the weakest option. Choose (1): document that _price is the storage attribute and price is the public property, and add a test that constructs every subclass through its public constructor and checks the invariant.
Under a dataclass, __post_init__ runs after all fields are set, so DiscountedProduct.__post_init__ can validate using _discount after calling super().__post_init__(). That removes the ordering hazard for validation, but it does not fix a base property setter that mutates derived state during field assignment.
3. Why is `tags = []` shared across all 50,000 instances, and how do you fix it in a plain class, in a dataclass, and under `__slots__`? What related mistake should you check for?
Read the worked answer
tags = [] is evaluated once when the class body executes; every instance that does not assign its own tags reads the same list, so product.tags.append('sale') mutates all 50,000 instances. The fix is per-instance initialization: def __init__(self, ..., tags=None): self.tags = list(tags) if tags is not None else []. Do not use tags=[] as a default argument either - the same shared-object bug applies, and the default is evaluated at function definition time. With a dataclass, use tags: list[str] = field(default_factory=list); field(default=[]) raises ValueError: mutable default <class 'list'> for field tags is not allowed. If you use __slots__, include 'tags' or the assignment raises AttributeError.
Copy the incoming iterable rather than storing it directly, so the caller cannot mutate the product's tags through the original list. Finally, check for other class-level mutables (dict, set, custom objects) in the same class; they have the same lifetime. A quick test is to create two instances, mutate one, and assert the other is unchanged.