What does collections.UserDict do differently from subclassing dict, and what does it cost?
answer
- It contains a dictionary rather than being one
- Every method is Python, so overrides always run
- Look for the wrapped mapping attribute
- isinstance against the built-in type fails
- Pay in speed and in type checks
basics
~20 scollections.UserDict stores a real dictionary in its data attribute and implements every mapping operation in Python on top of getitem and setitem, so one override covers the whole surface. The cost: it is not an instance of dict, and it is slower.
solid answer
~40 s`collections.UserDict` composes rather than inherits: it keeps a plain dictionary in `self.data` and implements the mapping operations in Python, routing them through `__getitem__`, `__setitem__`, `__delitem__`, `__iter__` and `__len__`. Because `UserDict.__init__` populates itself by calling `update()`, which assigns item by item, even keys passed to the constructor pass through your override — the exact hole that a `dict` subclass leaves open. You pay twice for that. First, `isinstance(u, dict)` is `False`, so C-level consumers that type-check for a real dictionary reject it — `json.dumps()` raises `TypeError` on one, for instance. Second, every operation is a Python-level call rather than C, so lookups are measurably slower. Use it when correct interception matters more than speed or than passing as a `dict`.
code
python · 10 linesfrom collections import UserDict
class Tracked(UserDict):
def __setitem__(self, key, value):
print("tracked", key)
self.data[key] = value
u = Tracked({"a": 1}) # prints: tracked a (UserDict.__init__ calls update)
u.update({"b": 2}) # prints: tracked b
print(u.data, isinstance(u, dict))go deeper
Know that the standard library ships a wrapper class for building custom mappings, that it keeps a real dictionary inside itself, and that this is the recommended alternative to subclassing the built-in type.
Explain the mechanism: Python-level methods funnelling through the primitive operations, construction routed through update, and the two costs — failing an isinstance check against the built-in type, and slower per-operation cost.
Judge which base fits a given consumer set. Show you have hit a library that type-checks for the concrete built-in, and say how you decided between wrapping, inheriting with a full override surface, or refusing to expose a mapping at all.
Frame it as public-contract design: a container base class advertises an entire protocol to every future caller. Decide whether your type should promise mapping semantics at all, and what that promise costs when the implementation later changes.
## What it actually is `collections.UserDict` is a mapping implemented in Python that *contains* a dictionary instead of *being* one. Its instances carry a `data` attribute holding an ordinary `dict`, and the class derives from `collections.abc.MutableMapping`, so the whole mapping surface — `get`, `pop`, `items`, `values`, `setdefault`, `update`, `==` — is built on top of a handful of primitive operations that are themselves ordinary Python methods. That is the entire trick. Override `__setitem__` on a `UserDict` and *every* write path lands in it, because every write path is Python code that ends in `self[key] = value`. Its `__init__` is the clearest case: it sets `self.data = {}` and then calls `self.update(...)` with whatever you passed, so construction respects the override too. ```python from collections import UserDict class Tracked(UserDict): def __setitem__(self, key, value): print("tracked", key) self.data[key] = value u = Tracked({"a": 1}) # tracked a u.update({"b": 2}) # tracked b ``` Compare with a `dict` subclass, where only literal subscript assignment reaches the override and the constructor, `update()`, `setdefault()` and `|=` do not. It also honours `__missing__`: `UserDict.__getitem__` looks in `self.data`, and if the key is absent and the class defines `__missing__`, it calls it before raising `KeyError`. So the same miss hook works whichever base you pick. `collections.UserList` is the same idea for lists, and `collections.UserString` for strings. ## What it costs **It is not a dict.** `isinstance(UserDict(), dict)` is `False`. That matters more than it sounds, because plenty of code — especially C-implemented code — fast-paths on the concrete type rather than duck-typing: ```python import json from collections import UserDict json.dumps(UserDict({"a": 1})) # TypeError: not JSON serializable ``` Anything that does `isinstance(x, dict)`, or a C extension that calls a dict-specific API, will reject or mishandle it. The standard workaround is to hand over `u.data`, or to give your class a method that returns a plain dictionary. Note the flip side: `UserDict({"a": 1}) == {"a": 1}` *is* `True`, because equality is defined structurally over the mapping — so a type check and an equality check disagree about the same object, which is exactly the sort of thing that hides in a code path for months. **It is slower.** Each read is a Python method call that then indexes the wrapped dictionary, instead of one C-level hash lookup. In a simple microbenchmark on CPython 3.14 the indexing operation costs roughly twice a plain `dict`'s, and the gap widens for the derived operations built out of primitives. For a mapping holding configuration or a few hundred entries touched occasionally, that is irrelevant; for the hot lookup in an inner loop, it is not. **It has a second surface.** `self.data` is public and writable. Nothing stops a caller — including a careless method of your own subclass — from writing `u.data[key] = value` and bypassing the very interception you built the class for. ## Choosing between the options Ask what the object must *be*, not what it must *do*. * If it must be accepted anywhere a real dictionary is — passed to a C extension, serialized by a library that type-checks, used as a class `__dict__`-like payload — subclass `dict` and accept that you must override every method you care about. * If correct, total interception is the point and speed is not critical, use `UserDict`; one override covers the surface. * If your object is not really a mapping — it has three domain operations and a dictionary happens to be the storage — do not subclass either. Hold the dictionary as a private attribute and publish only the operations you mean. This is usually the right call, and interviewers asking about `UserDict` are frequently probing for it. ## The interview framing The question behind the question is composition versus inheritance, made concrete by a language quirk. A good answer names the mechanism (`self.data`, Python-level methods, `__init__` routing through `update`), names both costs (not a `dict` instance, slower), and closes on the design point: extending a built-in container by inheritance means signing a contract you do not fully control, while wrapping means writing more code but knowing exactly what your object promises.
- Why does a UserDict subclass's __setitem__ run for keys passed to the constructor?Because `UserDict.__init__` does not populate storage directly: it sets `self.data = {}` and then calls `self.update(...)`, and that update assigns item by item through `self[key] = value`. Every write therefore lands in `__setitem__`. A `dict` subclass has no such guarantee — its inherited C-level `__init__` fills the table without consulting the override.
- A library rejects your UserDict because it type-checks for dict. What are your options?Hand it `u.data`, the real dictionary underneath, or expose a method returning `dict(self)`. If the rejection is pervasive across your dependencies, that is evidence the object needs to *be* a `dict` — switch to a `dict` subclass and override the write surface explicitly, or drop the mapping pretence and expose a narrow domain API instead.
- Does UserDict support __missing__?Yes. `UserDict.__getitem__` checks the wrapped dictionary, and if the key is absent and the class defines `__missing__`, it calls it before raising `KeyError`. So the miss hook behaves the same whether you wrap or inherit; the difference between the two bases is on the write side, not the miss side.
saying these in an interview costs you the question
- Thinks a UserDict instance passes an isinstance check for dict
- Claims UserDict is just a slower alias for dict
- Cannot name the wrapped dictionary attribute
- Ignores that the wrapped mapping is publicly writable
- Reaches for a container base class when composition is simpler