How does operator.attrgetter('user.plan.name') resolve its dotted path?
answer
- The attribute twin of the item fetcher
- A dot in the string means traverse
- Repeated getattr, one segment at a time
- No third argument for a default
- Properties along the path actually execute
basics
~20 sIt walks the path one segment at a time with repeated attribute lookups at call time, equivalent to obj.user.plan.name. A dot inside the string means traversal, never an attribute whose own name contains a dot.
solid answer
~40 s`operator.attrgetter` splits the string on dots at construction time and, on each call, performs a chained lookup: fetch `user` from the operand, then `plan` from that, then `name` from that — exactly what `obj.user.plan.name` does, and exactly as failure-prone. Any segment that is missing, or whose owner is `None`, raises `AttributeError` at that point; unlike the built-in `getattr`, `attrgetter` has no default argument to soften it. Every segment goes through normal attribute access, so a `property` or any other descriptor along the path really runs, side effects included. Pass several names and you get a tuple back in the order given — `attrgetter('user.id', 'status')` returns `(obj.user.id, obj.status)` — while a single name returns the bare value. It reads attributes only; dictionary keys are `operator.itemgetter`'s job.
code
python · 15 linesfrom operator import attrgetter
from types import SimpleNamespace
acct = SimpleNamespace(
status="active",
user=SimpleNamespace(id=7, plan=SimpleNamespace(name="pro")),
)
print(attrgetter("user.plan.name")(acct))
print(attrgetter("status", "user.id")(acct))
try:
attrgetter("user.discount.rate")(acct)
except AttributeError as exc:
print("failed:", exc)go deeper
Know that the string names an attribute and that a dot inside it walks to a nested object. Be able to say what a single name returns versus two names.
Explain the chained-getattr expansion, that traversal happens per call, that a missing or None segment raises AttributeError, and that there is no default parameter the way the built-in getattr has one.
Show that you think about what runs during the walk: a property or lazy descriptor on the path executes on every element of a large collection, and an optional segment makes the getter the wrong tool.
Own the data-shape argument — deep dotted paths hard-code one object graph into call sites everywhere, so treat a proliferating path as a signal to expose a flat accessor on the domain object instead.
`operator.attrgetter` is the attribute-side twin of `operator.itemgetter`. You give it one or more attribute names as strings, and it returns a callable that fetches them from whatever operand you later hand it. Its distinguishing feature — the thing interviewers actually probe — is that a name may contain **dots**, and a dot means *traverse*, not *part of the name*. ## What the dotted form expands to `attrgetter('user.plan.name')` is functionally `lambda obj: obj.user.plan.name`, or more explicitly `getattr(getattr(getattr(obj, 'user'), 'plan'), 'name')`. - The string is split on `.` when the getter is built; - the traversal itself happens on every call. There is no attribute literally named `'user.plan.name'` being looked up — Python identifiers cannot contain dots, so there would be nothing to find, and the only way to reach such a key would be a dict, which is not what this tool touches. ## Chained lookup means chained failure Each step is an ordinary attribute access, so it fails the ordinary way. - If `obj.user` is `None`, the second step raises `AttributeError: 'NoneType' object has no attribute 'plan'`. - If `plan` exists but `name` does not, you fail on the third step. The exception tells you which object lacked which attribute, which is genuinely useful when debugging, but note what `attrgetter` does **not** give you: there is no default. The built-in `getattr` takes an optional third argument and returns it instead of raising; `attrgetter` has no equivalent, so an optional path — a subscription record whose `.discount` is sometimes absent — is not something this factory can express. Reach for a named function with a `try`/`except AttributeError`, or normalise the objects so the attribute always exists. ## Descriptors along the path really run Attribute access is not a passive read in Python. If `plan` is a `property`, or any other descriptor, evaluating it executes its getter. So `attrgetter('user.plan.name')` may trigger a computation, a lazy load, or a logging side effect at each hop, and it will do so on every call because nothing is memoised. That matters when the getter is applied across a large collection: a dotted path over a lazily-computed property is a hidden loop of real work, not three pointer follows. Class attributes, instance attributes and attributes served by a custom `__getattr__` are all reached the same way, since the getter uses the normal protocol rather than poking at `__dict__`. ## One name versus several Like `itemgetter`, the arity changes the result shape. - `attrgetter('status')(obj)` returns the bare value. - `attrgetter('status', 'user.id')(obj)` returns the tuple `(obj.status, obj.user.id)`, in the order you wrote the names — dotted and plain names mix freely in the same call. That multi-name form is the reason the factory exists at all: it expresses a compound extraction from an object in a single, declarative expression. ## Where it sits against its neighbours `attrgetter` reads attributes; `operator.itemgetter` subscripts. Apply `attrgetter('plan')` to a dict and you get `AttributeError`, because a dict's keys are not attributes — a very common mix-up when the same data arrives sometimes as records and sometimes as mappings. A `namedtuple` or a dataclass is the case where both would work: the field is a real attribute, so `attrgetter('amount')` reads it, and a namedtuple additionally supports `itemgetter(2)` by position. Prefer the attribute form there; it survives fields being reordered. ## Why not just write it out Three reasons, the same three that justify the whole module. 1. The type is implemented in C in CPython, so calling it does not push a Python frame — a small but real saving when the getter runs over a long sequence. 2. It states intent declaratively: the names are data, visible in the expression, with no function body to read. 3. And because it is an instance of a genuine, importable type rather than an anonymous function, it can be serialized and handed to another process. The place it turns up most is the `key=` argument of sorting and `min`/`max` helpers over collections of objects, and in `itertools.groupby`, where the same getter is naturally used to both sort and group. ## The honest limits It can only read. It cannot call, transform, coerce case, or supply a fallback, and the moment you want any of those the clear move is an ordinary named function. Building `attrgetter` on a path you do not control is also fragile in a way `getattr` with a default is not — an optional middle segment turns into a crash rather than a `None`.
- What does operator.attrgetter give you that the built-in getattr does not, and what does getattr give that it does not?`attrgetter` adds two things: a dotted path that traverses several hops in one string, and a multi-name form that returns a tuple. It also produces a reusable callable rather than performing one lookup. `getattr` has the thing `attrgetter` lacks entirely — an optional third argument used as a default when the attribute is missing. So an optional field is `getattr`'s case, and a fixed deep path applied to many objects is `attrgetter`'s.
- Why can operator.attrgetter('plan') not read the 'plan' key of a dict?Because it performs attribute access, and a dict's entries are keys, not attributes — `attrgetter('plan')` on a dict raises `AttributeError`. Subscription is `operator.itemgetter`'s job, so `itemgetter('plan')` is the mapping form. The two are easy to swap by accident when the same records arrive sometimes as objects and sometimes as decoded mappings; picking the wrong one fails loudly rather than silently, which is the one mercy here.
- Is anything looked up when attrgetter('a.b.c') is constructed?No object is touched. Construction stores the names and splits the string on dots; the traversal runs on each call, so the same getter is reusable and a broken path only raises when it is finally applied. That also means each call re-walks the chain — nothing is cached — which is worth remembering when a segment is a property that does real work.
saying these in an interview costs you the question
- Thinks attrgetter('a.b') looks up one attribute named 'a.b'
- Expects a default argument like getattr's third parameter
- Says attrgetter reads dictionary keys
- Believes the path is resolved when the getter is built
- Cannot say what several names return
- Assumes properties on the path are not executed