skip to content

When is operator.attrgetter a better sort key than an equivalent lambda?

level: middleimportance: nice to knowfreq 22%

answer

  1. It returns a callable, not a value
  2. Dots inside the name mean something
  3. Several names give one result object
  4. Survives being sent to another process
  5. No default, no transformation

basics

~20 s

operator.attrgetter builds a C-level callable that fetches named attributes: it walks dotted paths, returns a tuple for several names, and can be pickled — none of which a lambda offers. A lambda still wins when the key needs a transformation.

solid answer

~40 s

`operator.attrgetter("ts")` returns a callable that does `obj.ts`, built in C rather than as a Python function, so it carries less per-call overhead than `lambda o: o.ts` in a sort over a large list. Three capabilities are what actually decide it: it accepts a dotted path — `attrgetter("author.name")` chains the lookups — it accepts several names and returns them as a **tuple**, which is exactly the key a multi-column sort wants, and instances of it are picklable, so a key function can cross a process boundary where a lambda raises. Its limits are equally clear: no default, so a missing attribute raises `AttributeError`, and no room for a transformation, so `lambda o: o.name.lower()` has no `attrgetter` equivalent. Sibling factories `operator.itemgetter` and `operator.methodcaller` cover subscripts and method calls.

code

python · 21 lines
python
import pickle
from operator import attrgetter

class Author:
    def __init__(self, name):
        self.name = name

class Msg:
    def __init__(self, ts, author):
        self.ts = ts
        self.author = author

rows = [Msg(2, Author("bo")), Msg(1, Author("al"))]
print([m.ts for m in sorted(rows, key=attrgetter("ts"))])
print(attrgetter("author.name")(rows[0]))
print(attrgetter("ts", "author.name")(rows[0]))
print(pickle.loads(pickle.dumps(attrgetter("ts")))(rows[0]))
try:
    attrgetter("edited_at")(rows[0])
except AttributeError as exc:
    print("AttributeError:", exc)

go deeper

for a junior

Recognise it as a factory that returns a callable fetching a named attribute, and know it is the usual key argument to sorted alongside a lambda.

for a middle

Be able to name the three capabilities a lambda lacks — dotted paths, tuple keys from several names, and picklability — and the two it lacks in return: no default and no transformation of the value.

for a senior

Show where it changes a design rather than a line: key functions built from configuration strings, registries mapping a sort name to a getter, and key functions that must survive being pickled to worker processes.

for a principal

Own the wider point that access paths can be data. Decide where a configuration-driven getter is a clean extension point and where it becomes an unreviewable mini-language your team has to debug at run time.

## What the factory produces `operator.attrgetter(name)` does not fetch anything. It *builds and returns a callable* which, applied to an object later, performs the attribute lookup. It is the reusable, first-class form of "fetch this named attribute", where the builtin `getattr` is the one-shot form. The name is fixed when the callable is constructed; the lookup happens when it is called. That makes it the natural argument for every higher-order function that takes a key: `sorted`, `min`, `max`, `itertools.groupby`, and any of your own APIs that accept a key function. ## The three things it does that a lambda cannot **Dotted paths.** `attrgetter("author.name")` is `obj.author.name` — the factory splits on the dot and chains the lookups. Encoding a path as data this way is how a generic sort or grouping helper accepts "sort by this field" from configuration without `eval` and without hand-written chains. **Tuple keys from several names.** Given more than one name, the callable returns a tuple of the values in the order requested: ```python sorted(rows, key=attrgetter("day", "author.name")) ``` That is a multi-key sort expressed in one call, and it relies on tuples comparing lexicographically. Writing it as a lambda means building the tuple by hand, which is more code doing exactly the same thing. **Picklability.** An `attrgetter` instance can be pickled; a lambda cannot. That matters the moment a key function has to cross a process boundary — a `ProcessPoolExecutor` or a `multiprocessing` pool serialises what it sends to the workers, and a lambda raises there while an `attrgetter` travels. The speed argument is real but secondary. The callable is implemented in C, so it avoids the Python-level frame that a lambda pays for on every element. On a large sort that is a measurable few percent, not a transformation of the program's performance, and it should never be the headline reason you reached for it. ## What it deliberately cannot do There is **no default**. `attrgetter("edited_at")(msg)` raises `AttributeError` when the attribute is missing, with no third-argument escape hatch of the kind `getattr` offers. If your collection is heterogeneous, the honest key is `lambda o: getattr(o, "edited_at", None)` — and even then you have to think about whether `None` sorts sensibly against your other values. There is **no transformation**. The callable returns the attribute exactly as it is. Case-insensitive sorting, coercions, `None`-handling, computed keys: all of those need a real function. A lambda that merely reads an attribute is a candidate for replacement; a lambda that does anything to the value is not. The name is also **fixed at construction**, which is the point — it is a specialised getter, not a general dynamic accessor. When the name varies per call, you want `getattr` directly. ## Its siblings, and choosing between them The `operator` module offers the matching factories for the other two access shapes. `operator.itemgetter(k)` does `obj[k]`, which is what you want for dictionaries, rows and sequences — and it takes multiple keys with the same tuple behaviour. `operator.methodcaller("lower")` calls a named method with fixed arguments. Choosing between them is choosing between attribute access, subscription and method invocation; they are not interchangeable, and reaching for the wrong one on an object that supports both attribute and item access is a real source of confusion when the two do not carry the same data. ## The interview signal This is a differentiator question, not a gate. What it is really probing is whether you see key functions as *data* — objects that can be built from a configuration string, composed, stored in a registry mapping a sort name to a getter, and shipped to a worker process — rather than as one-off lambdas typed at the call site. The candidate who answers only "it is faster than a lambda" has the least interesting third of the answer.

  • What does operator.attrgetter do when you give it more than one name?
    It returns a tuple of the values, in the order the names were given. Since tuples compare lexicographically, that is precisely a multi-key sort key: `sorted(rows, key=attrgetter("day", "author.name"))` sorts by day and breaks ties by author name, with no hand-built tuple at the call site.
  • How would you express a sort key over an attribute that may be missing?
    Not with `attrgetter` — it has no default and raises `AttributeError`. Use `lambda o: getattr(o, name, sentinel)` and choose a sentinel that orders correctly against the real values; sorting a mix of `None` and numbers raises `TypeError` on comparison, so the sentinel has to be a value of the same comparable type.
  • When does the difference between attrgetter and a lambda actually break a program rather than just slow it?
    When the key function has to be serialised. A process pool pickles what it sends to its workers, and a lambda is not picklable, so the submission fails outright; an `attrgetter` instance pickles and travels fine. The same applies to storing a key in anything that persists callables.

saying these in an interview costs you the question

  • Thinks attrgetter fetches the value immediately
  • Believes attrgetter accepts a default like getattr
  • Assumes a dotted name means one literal attribute
  • Says the only difference from a lambda is speed
  • Confuses attrgetter with itemgetter for subscripting

context