What does `dict.keys()` return in Python 3, and how does that object behave as the dict changes?
answer
- Not a list, and not a copy
- It watches the dict
- Set operators work on two of three
- dict_keys is not subscriptable
basics
~20 sA dict_keys view object, not a list. The view is a live window onto the dict, so keys added or removed later show through it. It supports len, iteration, fast membership and set operations, but not indexing.
solid answer
~40 s`keys()`, `values()` and `items()` return the view types `dict_keys`, `dict_values` and `dict_items`. A view holds a reference to the dict rather than a copy, so it is dynamic: mutate the dict and the view reflects it immediately, and creating one costs no memory proportional to the dict. All three support `len()`, iteration and `in`; none support indexing, so `d.keys()[0]` raises `TypeError`. `dict_keys` and `dict_items` are set-like and support `&`, `|`, `-` and `^` against sets and other views, which is the neat way to diff two dicts. `dict_values` is not set-like - values are neither unique nor guaranteed hashable - and `x in d.values()` is a linear scan rather than a hash lookup. For a stable snapshot, call `list(d)`.
code
python · 10 linesconfig = {"rate": 1200, "region": "eu"}
keys = config.keys()
config["shard"] = 3
print(list(keys))
print(keys & {"rate", "missing"})
print(sorted(keys - {"rate"}))
try:
keys[0]
except TypeError as exc:
print(exc)go deeper
Recall that keys() does not give you a list: you cannot index it or slice it, and list(...) is what turns it into one. Knowing that much keeps you out of the common d.keys()[0] TypeError.
Explain the mechanics an interviewer is after: a view holds the dict rather than a copy, so it is live and cheap, and keys and items are set-like while values are not. Say why values cannot be.
Demonstrate using views deliberately - diffing two configurations with set algebra, passing a view instead of a materialized list to avoid a copy, and knowing exactly when you must snapshot with list because the dict is about to change.
Frame it as an API question. When you expose a mapping from a library, decide whether callers get a live view, a copy or a read-only wrapper, and document it: handing out a live view leaks mutability and couples callers to your internal state.
`dict.keys()`, `dict.values()` and `dict.items()` return **view objects** — instances of `dict_keys`, `dict_values` and `dict_items` — not lists and not copies. A view is a thin window onto the dict that created it: it stores a reference to the dict and computes everything on demand. That single design choice explains everything else about them. **Views are live.** Because a view holds the dict rather than a snapshot of it, any change to the dict is visible through a view you obtained earlier: ```python config = {"rate": 1200} keys = config.keys() config["region"] = "eu" list(keys) # ['rate', 'region'] ``` This surprises people who reach for `keys()` expecting the Python 2 behaviour, where it returned a list built at call time. In Python 3 the list-returning methods are gone and the `iterkeys` / `itervalues` / `iteritems` variants are gone with them, because a view already gives you what an iterator gave and more. If you want a snapshot — and you do want one before mutating the dict inside a loop — materialize it explicitly with `list(d)` or `list(d.items())`. **Views are cheap.** Creating one allocates a small object regardless of the dict's size; there is no O(n) copy and no memory proportional to the number of keys. Passing `d.keys()` to a function that only iterates it is strictly better than passing `list(d.keys())`. **What a view supports.** All three support `len()`, iteration, and membership with `in`. Membership on `dict_keys` is a hash lookup, effectively O(1), and identical in cost to `key in d` — which is the more idiomatic spelling, so `if k in d.keys()` is redundant. Membership on `dict_values` is a linear scan, O(n), because values are not indexed at all; a service that looks values up by value repeatedly should build an inverted dict rather than scanning. None of the views support indexing or slicing: `d.keys()[0]` raises `TypeError: 'dict_keys' object is not subscriptable`. When you genuinely need positional access, wrap in `list` first, or use `next(iter(d))` for just the first key. **Set algebra.** `dict_keys` and `dict_items` are *set-like*: they support `&`, `|`, `-` and `^` against other views and against real sets, returning ordinary `set` objects. This is often the neatest way to compare two dicts: ```python old.keys() & new.keys() # keys in both new.keys() - old.keys() # keys added {k for k in old.keys() & new.keys() if old[k] != new[k]} # changed values ``` Keys can behave as a set because dict keys are hashable and unique by construction. Items can, provided the values are hashable too — `d.items() & other` over a dict whose values are lists raises `TypeError`, because the `(key, value)` tuple cannot be hashed. `dict_values` is deliberately **not** set-like: values are neither unique nor necessarily hashable, so `d.values() & {1}` raises `TypeError: unsupported operand type(s)`. Comparing two `dict_values` objects with `==` does not do an element-wise comparison either; it falls back to identity, so two distinct dicts with equal values are not "equal values" by that test. Compare `list(a.values()) == list(b.values())` or compare the dicts themselves. **Registration.** The three view types are registered with the abstract base classes `collections.abc.KeysView`, `collections.abc.ItemsView` and `collections.abc.ValuesView`, so `isinstance(d.keys(), collections.abc.KeysView)` is `True`. Those ABCs are also what you inherit from when writing your own mapping type, which is how a custom mapping earns the same view semantics. **Ordering.** Iterating any of the three yields entries in insertion order, guaranteed since Python 3.7, and `zip(d.keys(), d.values())` is guaranteed to pair correctly as long as the dict is not modified in between — which is exactly what `d.items()` gives you more directly. **The failure mode to remember.** Because a view is live, iterating one while adding or removing keys is the same as iterating the dict itself and raises `RuntimeError`; taking `d.keys()` first does not protect you. The fix is a real copy: iterate `list(d)`. In interviews, the question is usually phrased as "what does `keys()` return?" and the weak answer is "a list". The strong answer names the view type, says it is live rather than a copy, mentions the set operations on keys and items, and notes the one thing views cannot do — indexing. **Iterating well.** `for k in d` iterates the keys, which is why `for k in d.keys()` is redundant. When you need both halves, `for k, v in d.items()` is the form to use: it yields the pair straight out of the hash table, while `for k in d: v = d[k]` hashes every key a second time to fetch the value it already had in hand. For a large dict in a hot loop that is a measurable difference, and it is the first thing a reviewer will point at. If you need only the first key, `next(iter(d))` avoids materializing anything at all; if you need positional access to many keys, materialize once with `list(d)` rather than repeatedly.
- Why can `dict.items()` take part in set operations while `dict.values()` cannot?Keys are unique and hashable by construction, so a keys view can act as a set; an items view can too, provided the values are hashable - `d.items() & other` raises `TypeError` when a value is a list. Values have neither guarantee: they may repeat and may be unhashable, so `dict_values` supports only iteration, `len()` and `in`, and that `in` is a linear scan.
- Is `if k in d.keys()` different from `if k in d`?Only in noise. Both are hash lookups with the same cost, since `in` on a dict already tests keys and `dict_keys` membership delegates to the same table. `k in d` is the idiomatic spelling. The one that genuinely differs is `k in d.values()`, which scans every value in O(n).
- How do you compare two dicts to find added, removed and changed keys?Use set algebra on the keys views: `new.keys() - old.keys()` for additions, `old.keys() - new.keys()` for removals, and `{k for k in old.keys() & new.keys() if old[k] != new[k]}` for changed values. All three return ordinary sets, so the results are unordered - sort them if the output order matters.
A view is a window onto a room, not a photograph of it: move the furniture and the window shows the new arrangement, while a photograph would still show the old one.
saying these in an interview costs you the question
- Says `dict.keys()` returns a list you can index
- Thinks a view is a snapshot taken when the method was called
- Believes `list(d.keys())` is required just to iterate a dict
- Claims `dict.values()` supports set operators like `&`
- Assumes `x in d.values()` is a constant-time lookup
- Thinks taking a view first makes mutation during iteration safe