When should you expose config as a types.MappingProxyType rather than a copy?
answer
- Window versus photograph
- Live view or stable snapshot
- Constant-time wrapper versus linear copy
- Neither one is a security boundary
- Rebind a new mapping instead of editing keys
basics
~20 sUse the proxy when callers should see live state and must not write it: it costs one wrapper object and no copying. Use a copy when callers need a stable snapshot or their own mutable working set, and accept the per-call cost.
solid answer
~50 sThe two differ on three axes. **Liveness**: a `types.MappingProxyType` tracks the underlying mapping, so a reload is visible to every holder immediately; `dict(config)` freezes the contents at the moment of the call and goes stale. **Cost**: the proxy is one small object created in constant time and shares the storage; a copy is linear in size, per caller, per call. **Safety**: neither is a security boundary — the proxy protects against accidents only, and the wrapped mapping is still reachable in-process, for instance through `gc.get_referents`. My default is a proxy over immutable values, exposed as a module-level or property accessor and typed `Mapping[str, object]`, with updates applied by building a *new* dict and rebinding the proxy rather than mutating in place, so readers never observe a half-applied change. I hand out copies only where a caller legitimately mutates its own working set.
code
python · 15 linesfrom types import MappingProxyType
class Settings:
def __init__(self, initial):
self._data = dict(initial)
self.view = MappingProxyType(self._data)
def reload(self, new_values):
self._data = dict(new_values) # build it whole
self.view = MappingProxyType(self._data) # then rebind, one statement
settings = Settings({"batch": 500})
pinned = settings.view
settings.reload({"batch": 250})
print(pinned["batch"], settings.view["batch"]) # 500 250go deeper
Know the two options exist and what each means: a proxy lets callers read the current values without writing them, and a copy gives them their own dict that stops tracking the original.
Be able to argue cost and liveness: constant-time wrapper sharing storage versus a linear copy per call, and a live view versus a snapshot that goes stale after a reload.
Demonstrate the operating judgment: pin a snapshot per unit of work, rebind a whole new mapping on reload instead of editing keys under readers, and state plainly that neither option is a security boundary.
Own the boundary policy across services: which shared state is exposed at all, whether it is immutable by construction, how reloads become visible, and what you promise callers about staleness and consistency.
## The question behind the question Interviewers ask this to see whether you reason about shared mutable state or just reach for the nearest idiom. There are really four options on the table — hand out the dict itself, hand out a `MappingProxyType`, hand out `dict(config)`, or hand out `copy.deepcopy(dict(config))` — and each answers a different question. ## Liveness A proxy is a window, so every holder sees the current contents. That is what you want for settings a running process can reload, feature flags, or a registry that grows as plugins import. A copy is a photograph: whoever took it keeps working from the state at that instant, which is what you want inside a single unit of work that must be internally consistent — a request handler, a batch, a reconciliation pass — because a config that changes mid-operation produces results nobody can explain afterwards. Take a copy at the start of the operation, use it for the duration, and let the next operation pick up the new values. ## Cost The proxy allocates one wrapper regardless of the mapping's size, and reads through it are a forwarded lookup. Copying is linear in the number of entries. That difference only matters when it is on a hot path, and the honest senior answer says so: for a 40-key settings dict read once per request, copying is free in practice and the argument is about correctness, not speed. It stops being free when a large mapping is copied per item in a loop, which is where you see a proxy earn its place. ## What neither of them gives you **Neither is a security boundary.** In-process code can reach the wrapped mapping — `gc.get_referents(view)[0]` returns it directly — so a proxy stops mistakes, not adversaries. If the threat model includes hostile code inside the process, no wrapper helps. **Neither is atomic across multiple keys.** If you mutate the underlying dict key by key while other threads read through the proxy, readers can observe the half-updated state. The fix is not a lock around every read but a rebind: build the replacement mapping completely, then swap the attribute that holds the proxy in one assignment. Every reader then sees either the whole old config or the whole new one, and holders of the old proxy keep a coherent view until they refetch. A shallow copy has the same problem in reverse — it is atomic per call only if the underlying dict is not being mutated *while* you copy it. **Neither is deep.** Both stop at one level. A proxy over a mapping whose values are lists leaves those lists mutable; `dict(config)` produces a new mapping whose values are still the same shared objects. Only `copy.deepcopy(dict(config))` or immutable values close that. ## A worked shape An inventory sync between two systems reads a mapping describing a 17-service dependency graph, and its handlers must never edit it. The version I would ship: parse the config into a structure with tuples for the dependency lists, wrap it in a `MappingProxyType`, and expose it through a property. A reload builds an entirely new frozen structure and rebinds the property in one statement. A run that has begun keeps whatever object it read at the top of the run, so a partial-failure rollback unwinds against exactly the graph it planned against, and no handler can quietly drop a dependency. That combination — immutable values, a proxy for the read path, a whole-object swap for the write path, and a snapshot pinned per run — is the answer worth giving, because it names which mechanism solves which of the three problems. ## Practical trimmings Annotate the exposed attribute as `Mapping[str, object]`, not `dict`, or a type checker will reject callers that pass it on. Remember that `json.dumps` refuses a proxy and needs `dict(view)`, and that `copy.copy` and `copy.deepcopy` both fail on the proxy itself with a pickling error — convert first. And keep the private mapping actually private: if the attribute holding the underlying dict is public, the proxy is decoration. ## The failure to avoid The worst outcome is a codebase that returns the raw dict from a getter and calls it read-only in a docstring. Either the type system or the runtime should carry the claim; a comment cannot.
- A background thread reloads the settings while request handlers read them. What do you change?Never mutate the live mapping key by key, or readers can observe a half-applied config. Build the replacement mapping in full, then rebind the attribute holding the proxy in a single assignment, which readers see as all-or-nothing. Handlers that need internal consistency should also read the attribute once at the start of the request and pass that object down, rather than re-reading it at each use.
- Does wrapping the settings dict in a proxy protect it from untrusted code running in the same process?No. The wrapped mapping is still reachable — gc.get_referents on the proxy hands it back — and in-process code can reach the owning object's private attributes anyway. The proxy is an accident-prevention and intent-signalling tool. If untrusted code is your threat model, the boundary has to be a process or an interpreter, not an object wrapper.
- When is a shallow copy the wrong choice even though callers only read?When the mapping is large and copied on a hot path, when callers must see reloads immediately, or when the values are mutable and the copy creates the illusion of isolation it does not provide. In that last case the copy is worse than a proxy: it looks defensive while sharing every value object with the original.
A proxy is a live scoreboard everyone glances at; a copy is the printout you take into the meeting so the numbers stop moving while you argue about them.
saying these in an interview costs you the question
- Treats the proxy as a security boundary against hostile code
- Says a copy always isolates the caller, ignoring shared values
- Mutates the live mapping key by key under concurrent readers
- Returns the raw dict and calls it read-only in a docstring
- Annotates the exposed mapping as dict rather than Mapping
- Assumes the proxy makes reads thread-safe by itself