# Hooked Containers Hooked Containers adds optional, shallow mutation callbacks to familiar Python container operations. It provides `HookedMapping[K, V]`, `HookedList[T]`, and `HookedSet[T]`, backed by owned `dict`, `list`, and `set` storage. Requires Python 3.12 or newer. ## Mixin Design All reusable container behavior lives in `src/hooked_containers/mixins.py`. The design uses Python's [collections.abc abstract base classes](https://docs.python.org/3/library/collections.abc.html#collections-abstract-base-classes) directly. Three mutable mixins compose shared reads and hook dispatch with their required primitives; standard ABC methods supply the helpers: | Mixin | Backing Storage | Responsibility | | --- | --- | --- | | `ContainerMixin` | Any collection | Containment, iteration, length, and representation. | | `HookMixin` | None | Callback dispatch after successful writes. | | `ListMixin` | `list` | Indexed reads and writes, insertion, and native list operations. | | `MutableMappingMixin` | `dict` | Key-based reads and writes, copying, and merging. | | `MutableSetMixin` | `set` | Addition, discard, and named set operations. | `HookMixin` shares callback dispatch across the three mixins. Inherited methods such as `append`, `extend`, `reverse`, `update`, `clear`, and set in-place operators reach the primitive writes automatically. A compound operation may invoke the hook more than once. Each subclass initializes a single `_storage` attribute with the corresponding native container. There is no storage property or separate container-specific read-only hierarchy. Each of `sequence.py`, `mapping.py`, and `set.py` defines just one concrete wrapper, composing its mutable mixin and adding construction. Representation comes from `ContainerMixin`. Dict-style `popitem` intentionally removes the most recently inserted key, unlike the ABC's arbitrary-item implementation. Custom containers can use the mixins without any concrete wrapper: ```python from hooked_containers.mixins import MutableMappingMixin class Settings(MutableMappingMixin[str, int]): def __init__(self, hook=None): self._storage = {} self.hook = hook ``` ## Change Hooks A hook receives one immutable `Change(operation, key, value)` record after a write: | Field | Meaning | | --- | --- | | `operation` | `"add"`, `"set"`, or `"remove"` | | `key` | Mapping key, normalized list index, list slice, or `None` for sets | | `value` | Added/new value, or the value that was removed | ```python from hooked_containers import HookedList, HookedMapping, HookedSet def on_change(change): print(change.operation, change.key, change.value) items = HookedList(["first"], hook=on_change) items.append("second") # add 1 second items[0] = "updated" # set 0 updated del items[1] # remove 1 second settings = HookedMapping({}, hook=on_change) settings["enabled"] = True # add enabled True members = HookedSet(hook=on_change) members.add("ready") # add None ready members.discard("ready") # remove None ready ``` Reads never change data or invoke the callback. Missing mapping keys raise `KeyError`, and `get` respects its default. Integer list keys are normalized against the length before the write; insertions report their clamped destination. Slice keys preserve the supplied slice in pre-write coordinates, and their values are shallow lists of new or removed items. Sorting and in-place repetition report a whole-list `"set"` using `slice(None)`. Assignments notify even when assigning an equal value. Duplicate set additions, missing set discards, empty clears, and empty slice writes do not notify. Failed primitive writes do not notify; writes completed before a compound operation fails remain reported. A failed sort reports any resulting rearrangement. Callback exceptions propagate after the write without rolling it back. Callbacks that mutate the same container can trigger further callbacks; there is no batching or reentrancy guard. Construction copies the outer storage without notifying. Nested values are not copied or wrapped, and mutations inside those values are not observed. Copies, slices, and non-mutating derived results are ordinary native containers without hooks. Event values are shallow references, not deep snapshots; later changes to a mutable value remain visible through that reference. Direct changes to private storage bypass callbacks. Set comparison, algebra, and in-place methods follow the standard ABC implementations. In particular, ABC set operators accept iterables more broadly than built-in `set` operators. These wrappers implement container interfaces, not built-in subclass identity or exact built-in performance guarantees. ## Migration Custom subclasses now initialize `_storage` directly instead of implementing `_data`. The earlier container/collection hierarchy, read-only sequence, mapping, and set mixins, along with `MutableSequenceMixin`, have been replaced by the shared `ContainerMixin` and the three mutable mixins above. Import reusable mixins from `hooked_containers.mixins` and use native backing storage. The shallow API replaces the earlier root/path event subclasses and recursive wrapping. Use `change.operation`, `change.key`, and `change.value` instead of event types, `path`, and `item`. `HookedMapping` has independent key and value type parameters. Shared outer storage, autovivification, `suppress_hook`, `get_nested`, and the domain-state classes are no longer supported. The development notebooks are historical experiments with the previous API; the examples above describe the current API.