Hands-on operator.attrgetter vs builtin getattr lab: real ops/s for extract and sort by attribute versus direct access, measured on Linux localhost only.
4 min read
In a hot loop, is operator.attrgetter worth it over getattr, or should you just write .score? This lab times extract-to-list and list.sort(key=...) on Linux localhost for dataclass rows (plain + slots=True).
Related links:
Lab honesty (1 Oct 2026 IST): Python 3.13.5. N=50,000. Distinct from the itemgetter lab: focus is getattr builtin vs attrgetter vs direct attribute. Affiliates: 0.
Verdict up front: direct .score ~83M/s extract; attrgetter ~47M; getattr ~41M (direct ~2.0× getattr; attrgetter ~1.14× getattr). Sort gaps shrink: attrgetter vs getattr-lambda ~1.12×.
| Arm | Pattern |
|---|---|
| direct | [r.score for r in rows] / key=lambda r: r.score |
| attrgetter | map(attrgetter("score"), rows) / key=ag |
| getattr | [getattr(r, "score") for r in rows] |
| getattr default | getattr(r, "score", None) |
| lambda map | map(lambda r: r.score, rows) |
| dynamic name | name in a string; attrgetter(name) vs getattr |
Script: lab-evidence/65-attrgetter-vs-getattr/results/run_lab.py.
| Arm | ops/s | ns/op |
|---|---|---|
direct .score | 83,285,445 | 12.0 |
| slots direct | 89,706,534 | 11.1 |
| attrgetter map | 46,744,175 | 21.4 |
| slots attrgetter | 52,466,338 | 19.1 |
| getattr | 40,941,122 | 24.4 |
| getattr default | 39,318,995 | 25.4 |
| lambda map | 34,452,654 | 29.0 |
| attrgetter dynamic | 32,407,389 | 30.9 |
| getattr dynamic | 24,770,624 | 40.4 |
| Arm | elems/s | ns/elem |
|---|---|---|
key=attrgetter | 5,029,556 | 198.8 |
key=lambda r: r.score | 4,781,393 | 209.1 |
key=lambda getattr | 4,483,767 | 223.0 |
| attrgetter multi | 2,092,820 | 477.8 |
| lambda tuple | 1,912,574 | 522.9 |
.attr.List comprehensions and straight attribute loads do not need a callable. map, sorted/list.sort key=, and similar APIs do. That is attrgetter’s niche: a tiny C callable bound to one or more names, reusable without a Python lambda wrapper. getattr remains the right tool when the name is data (config key, optional field) — just do not pay for it on a static .score path. Slots rows in this lab were a bit faster on direct access (~90M/s) but the ranking vs getattr stayed the same.
.score or attrgetter.attrgetter(name) every call — bind once (dynamic arm builds once per timed run).| Need | Prefer |
|---|---|
| Fixed attribute in comprehension | direct .attr |
key= / map callable | operator.attrgetter |
| Name from config / optional field | getattr |
| Multi-field key | attrgetter("a", "b") |
Evidence: /workspace/lab-evidence/65-attrgetter-vs-getattr/results/.
Dots first; attrgetter for callables; getattr for dynamism. On this box direct extract ~2.0× getattr and ~1.78× attrgetter; sort attrgetter still ~1.12× a getattr lambda. Reach for getattr only when the attribute name is not known statically.
Lab evidence
Lab 1 Oct 2026 IST. Python 3.13.5; N=50000. extract direct 83M vs attrgetter 47M vs getattr 41M (direct2.0x getattr; ag1.14x getattr). sort attrgetter vs getattr ~1.12x. Affiliates: 0. Evidence: lab-evidence/65-attrgetter-vs-getattr/.
Hands-on operator.itemgetter vs lambda sort-key lab: real ops/s for extract and list.sort on tuples, dicts, and objects, measured on Linux localhost (lab).
Lab1 Oct 20264 min read
Hands-on fnmatch.filter vs re.compile name-list filtering measured on Linux localhost.
Lab1 Oct 20264 min read
Hands-on tarfile vs zipfile create+extract lab: real MB/s on a mixed small-file fixture (uncompressed tar vs zip), measured on Linux localhost for SREs.
Lab1 Oct 20264 min read