Why does a class's __dict__ come back as a mappingproxy rather than a dict?
answer
- The interpreter must know when a class changes
- Attribute lookups on types are cached
- Writes must go through setattr
- vars(SomeClass) returns the same object
- Instance __dict__ is a plain writable dict
basics
~20 sBecause the type machinery must be told about every change to a class namespace. Handing it out wrapped forces writes through setattr, which updates the internal bookkeeping CPython keeps for attribute lookup; a raw dict write would leave that stale.
solid answer
~40 s`SomeClass.__dict__` and `vars(SomeClass)` return a `mappingproxy`, so `SomeClass.__dict__["x"] = 1` raises `TypeError: 'mappingproxy' object does not support item assignment`. The reason is bookkeeping: CPython caches attribute lookups per type and mirrors dunder entries into internal slots, and both are refreshed by `type.__setattr__`. A direct write into the namespace dict would bypass that and leave the interpreter using stale information, so the namespace is only ever handed out read-only. The supported way to add or change a class attribute at runtime is `SomeClass.x = 1` or `setattr(SomeClass, "x", 1)`, which does the invalidation for you. Note the asymmetry: an *instance* `__dict__` and a *module* `__dict__` are ordinary dicts you may write to directly, because no such caching contract applies to them.
code
pycon · 16 lines>>> class Sync:
... retries = 3
...
>>> type(Sync.__dict__).__name__
'mappingproxy'
>>> Sync.__dict__["retries"] = 5
Traceback (most recent call last):
...
TypeError: 'mappingproxy' object does not support item assignment
>>> setattr(Sync, "retries", 5)
>>> Sync.__dict__["retries"]
5
>>> job = Sync()
>>> job.__dict__["retries"] = 9
>>> job.retries, Sync.retries
(9, 5)go deeper
Recall the practical rule: read a class namespace through dict if you like, but change class attributes with plain assignment or setattr, because writing into dict raises TypeError.
Explain the reason rather than the symptom: the interpreter keeps per-type lookup bookkeeping that only the official setattr path refreshes, so the namespace is handed out wrapped to keep it consistent.
Show where it matters in real code — decorators, plugin registries and introspection tools that must distinguish a class's own attributes from inherited ones, and that must mutate classes through the supported API.
Treat runtime class mutation as a design decision, not a trick: prefer explicit registration or composition over patching types in place, and know the cost when a library reaches for it.
## The observation ``` >>> class Sync: ... retries = 3 ... >>> type(Sync.__dict__) <class 'mappingproxy'> >>> Sync.__dict__["retries"] = 5 Traceback (most recent call last): ... TypeError: 'mappingproxy' object does not support item assignment >>> Sync().__dict__ {} >>> type(Sync().__dict__) <class 'dict'> ``` A class namespace is read-only through `__dict__`; an instance namespace is a plain, writable dict. `vars(Sync)` returns the same proxy — `vars` is just `__dict__` with a nicer spelling. ## Why classes are special Attribute lookup on a class is not a dict lookup. It walks the method resolution order, consults descriptors, and — crucially — CPython caches the result per type so that repeated lookups do not re-walk the MRO. That cache is only correct while the interpreter knows when a type changed. In addition, certain dunder entries are mirrored into internal fields on the type object when they are set: defining `__len__` or `__eq__` on a class does not merely put a function in a dict, it also updates the machinery that `len()` and `==` actually consult. All of that maintenance hangs off `type.__setattr__`. Set the attribute the normal way and the interpreter invalidates its caches and refreshes the mirrored entries. Poke a value directly into the namespace dict and none of it happens: the new attribute might not be visible to code that had already looked the name up, and a dunder assigned that way might never take effect at all. Rather than leave that trap open, CPython never hands the writable namespace out. The `mappingproxy` is the enforcement mechanism. ## The supported ways to change a class at runtime * `Sync.retries = 5` or `setattr(Sync, "retries", 5)` — both route through `type.__setattr__`, which performs the write and the invalidation. `delattr(Sync, "retries")` is the removal counterpart. * Reading is unrestricted: `Sync.__dict__["retries"]`, `"retries" in Sync.__dict__` and iteration all work, and reading the namespace directly is genuinely useful — it shows what *this* class defines without inherited names, which plain attribute access cannot distinguish. * During the class body's execution the namespace is a real, writable mapping; only the finished type wraps it. That is why a metaclass can prepare and populate a namespace, and why decorators are given the class object afterwards and use `setattr`. One related limit: built-in and extension types refuse attribute assignment entirely. `str.foo = 1` raises `TypeError` saying the type is immutable, which is a separate rule from the proxy — the proxy blocks the back door, `type.__setattr__` blocks the front door for those types. ## Reading a class namespace well Because the proxy is a live view, `Sync.__dict__` reflects later `setattr` calls without being refetched. It is the right tool when you must distinguish own attributes from inherited ones — a registry that collects only the methods a subclass actually defines, for instance, or introspection that reports which of a base class's hooks were overridden. `dir()` and `getattr` flatten the MRO; `__dict__` does not. When you want a mutable working copy of the namespace, `dict(Sync.__dict__)` gives you one, which you can then compare or diff without touching the class. ## Why it is a good interview question It is not high-frequency and nobody is rejected for missing it, but it connects three things a strong candidate should already hold separately: what a `mappingproxy` is, that classes keep their attributes in a namespace mapping, and that the interpreter maintains caches whose correctness depends on going through the official mutation path. Candidates who have hit it usually hit it while writing a decorator or a plugin registry, tried the direct dict write because it read more explicitly, and learned from the `TypeError` that `setattr` is the API.
- How do you add an attribute to a class at runtime, then?`SomeClass.x = value` or `setattr(SomeClass, "x", value)`, and `delattr` to remove one. Both go through type.__setattr__, which writes the namespace entry and refreshes the interpreter's per-type bookkeeping, so the new attribute is immediately visible to every lookup. Built-in and extension types are a separate case: they refuse attribute assignment outright, whichever spelling you use.
- Is an instance's __dict__ also a mappingproxy?No — it is an ordinary dict, and writing into it directly works and is equivalent to setting the attribute. Instances carry no per-type cache to invalidate, so nothing needs protecting. Classes that define __slots__ have no instance __dict__ at all, and a class that customises attribute storage may expose something else entirely.
- What can you learn from reading a class __dict__ that getattr cannot tell you?Which names the class itself defines, as opposed to inherits. Attribute access and dir() flatten the method resolution order, so they cannot distinguish an overridden hook from an inherited one. Reading the namespace directly is how registries and introspection tools collect only a subclass's own methods.
It is the difference between editing the library catalogue by hand and checking a book out at the desk: the desk updates the index, your pencil does not.
saying these in an interview costs you the question
- Claims class attributes cannot be changed at runtime
- Tries to add a method by assigning into the class __dict__
- Thinks an instance __dict__ is also read-only
- Says vars(SomeClass) returns a mutable dict
- Believes the proxy exists to make classes thread-safe