Two lookups that look interchangeable and answer different things. Read what each says about a field that is present and None.
record = {'name': 'ada', 'nickname': None}
print(record.get('nickname', 'MISSING')) # -> None present, so no default
print(record.get('absent', 'MISSING')) # -> 'MISSING' genuinely not there
print(record.get('nickname') or 'MISSING') # -> 'MISSING' wrong: it *was* there
print('nickname' in record) # -> True
- get’s default fires on absence, not on emptiness
That is exactly the behaviour this function needs, and it is why get with a default is the right tool rather than a check followed by a lookup. A field holding None is present, and get says so.
- `or default` asks a different question
It replaces anything falsy — None, 0, False, '' and empty containers. That turns 'this customer has no orders' into 'we do not know', which is not the same statement. It is the same trap as deep-get and first-match, and it keeps appearing because the shorter form reads so plausibly.
- The default is the caller’s, not yours
Taking it as an argument means the caller decides what absence should look like in their output — None for a report, 0 for a sum, an empty string for a CSV. Hard-coding one makes the function slightly easier to call and considerably less useful.
One entry per record, so a comprehension over the records. The only decision is how to read a field that might not be there — and three of the cases are about getting that decision right.
dict.get(field, default) returns the default only when the key is absent. Do not reach for `or` — that would also replace None, 0 and False, which are values the records may legitimately hold.
return [record.get(field, default) for record in records]. One line, no branching, and the awkward cases are handled by choosing the right lookup rather than by writing checks.
def pluck(records, field, default): return [record.____(field, ____) for record in ____]
def pluck(records, field, default):
return [record.get(field, default) for record in records]
record[field] would raise for a missing field; `record.get(field) or default` would replace legitimate falsy values; a check followed by a subscript would do two lookups and read worse. get with a default asks precisely the question the contract states, which is why there is nothing left to write.
Skipping records that lack the field would be a reasonable-looking alternative and would break every caller who expects to zip the result back against the records. Filtering and mapping are different operations; a function that quietly does both is hard to reason about.
Making default a required argument rather than defaulting it to None forces the caller to think about what absence should look like in their output. It is one more character at every call site and it removes a whole class of 'why is there a None in my CSV' surprise.
If you want only the records that have the field, filter first and pluck second — two clear steps beat one function with a hidden policy. If the records are objects rather than dictionaries, operator.attrgetter does the same job for attributes and composes with sorted and map. And for a large table of numbers, pulling a column out of pandas or a database query will be dramatically faster than a Python loop over dictionaries.
- Pluck several fields at once, returning a list of tuples, and decide what the default should look like when only some of them are missing.
- Support a dotted path by reusing deep-get, so pluck(users, 'address.city', None) works.
- Write a version that raises for a missing field instead of defaulting, then decide which of the two you would want when importing a file somebody else produced.