Plate 15
attrgetter vs getattr: Hot Loop Lab
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.
Aditya Challa4 min read
Intro — what this post promises
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:
- itemgetter vs lambda sort localhost lab
- Counter vs dict tally localhost lab
- dataclass vs slots vs dict localhost lab
- frozenset vs set membership localhost lab
- bytes vs bytearray localhost lab
- enum vs constants localhost lab
- itertools vs python loops localhost lab
- perf_counter vs time localhost lab
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×.
Arms
| 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 |
Lab topology
Script: lab-evidence/65-attrgetter-vs-getattr/results/run_lab.py.
Lead table — extract (p50)
| 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 |
Lead table — sort (p50)
| 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 |
Reading it
- Direct attribute wins extract — LOAD_ATTR beats a function call (~1.78× attrgetter, ~2.0× getattr).
- attrgetter beats getattr when you need a callable (~1.14× extract; ~1.31× with a dynamic name built once).
- Sort compresses gaps — Timsort dominates; attrgetter still edges getattr-lambda (~1.12×).
- getattr is for dynamic / optional names — not a faster
.attr.
Direct vs callable: when the API forces a function
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.
Pitfalls
- Using getattr for a fixed name in a hot loop — write
.scoreor attrgetter. - Rebuilding
attrgetter(name)every call — bind once (dynamic arm builds once per timed run). - Optimizing sort keys before N hurts — tens of ns inside a ms-scale sort.
- Confusing with itemgetter — tuples/dicts vs objects (see related itemgetter lab).
When to pick what
| 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") |
Reproduce
Evidence: /workspace/lab-evidence/65-attrgetter-vs-getattr/results/.
Closing
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
What I found running this
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/.
Related links
Plate 82
itemgetter vs lambda: Sort Key Lab
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).
Observability & SRE · 30 Sept 2026
Plate 18
fnmatch vs re Name Filter: Localhost Lab
Hands-on fnmatch.filter vs re.compile name-list filtering measured on Linux localhost.
Observability & SRE · 30 Sept 2026
Plate 84
tarfile vs zipfile Create+Extract: Localhost Lab
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.
Observability & SRE · 30 Sept 2026