Python get vs setdefault: Choosing the Right Dict Method
python get vs setdefault: Understand the behavioral difference between dict.get and dict.setdefault, when each mutates the dictionary, and how to choose based on inten...
When working with Python dictionaries, the choice between get and setdefault often comes down to whether you want to read a value without side effects or ensure a default exists before continuing. The python get vs setdefault decision affects both the dictionary's state and how clearly the code communicates its intent. Both methods return the value for a given key, but they differ in one critical way: setdefault can write to the dictionary, while get never does.
The Core Difference Between get and setdefault
dict.get(key, default) returns the value for key if it exists, otherwise it returns default. It never modifies the dictionary. dict.setdefault(key, default) does the same lookup, but if key is missing, it inserts key with default as the value and then returns that value.
inventory = {"apple": 3} print(inventory.get("banana", 0)) # 0 print(inventory) # {'apple': 3} print(inventory.setdefault("pear", 5)) # 5 print(inventory) # {'apple': 3, 'pear': 5}
The first call leaves the dictionary untouched. The second call adds a new key. That single behavioral difference drives most of the practical guidance around these methods.
What get Returns and What It Leaves Untouched
get is the safer choice when you only need to read a value and want to avoid accidental dictionary growth. Repeated calls to get with the same missing key return the default every time without accumulating entries.
config = {"timeout": 30} for _ in range(3): print(config.get("retries", 3)) print(config) # {'timeout': 30}
This makes get ideal for lookups where the dictionary represents external state, such as parsed configuration, request headers, or cached data. Using get guarantees that a read operation has no observable side effect on the data structure.
What setdefault Does When the Key Is Missing
setdefault is useful when you want to retrieve an existing value or create and store a default in one step. This is common when building nested structures like lists of items grouped by category.
groups = {} for item in ["a", "b", "a", "c"]: groups.setdefault(item, []).append(item) print(groups) # {'a': ['a', 'a'], 'b': ['b'], 'c': ['c']}
Each call checks whether the key exists. If it does, setdefault returns the existing value and does not replace it. If it does not, the default is stored and returned. The append then operates on the correct list in either case.
When setdefault Mutates the Dictionary and When It Does Not
A common point of confusion is that setdefault does not always write to the dictionary. It only inserts when the key is absent. If the key already exists, the dictionary is left unchanged and the existing value is returned.
settings = {"mode": "fast"} result = settings.setdefault("mode", "slow") print(result) # fast print(settings) # {'mode': 'fast'}
What many developers miss is that the default argument is evaluated eagerly, before setdefault decides whether it is needed. Every call to setdefault("key", []) constructs a new empty list, even when the key already exists and that list is discarded. The same applies to any expression passed as the default, so expensive computations in the default argument run on every call.
counts = {"x": 1} # A new list is created here even though "x" exists value = counts.setdefault("x", [])
This eager evaluation is a real runtime cost, especially in loops or hot paths. get has the same characteristic, but because get never stores the default, the discarded object is often less surprising.
Performance and Runtime Cost Considerations
Neither get nor setdefault performs a linear scan. Both rely on the dictionary's hash lookup, so the per-call cost is roughly constant for a given dictionary size. The meaningful performance difference comes from two factors: dictionary growth and eager default construction.
setdefault grows the dictionary when keys are missing. If you process a large dataset with many unique keys, the dictionary resizes over time, and each insertion has amortized cost. get never grows the dictionary, so repeated reads on missing keys do not change its size.
The eager evaluation of the default argument matters more than the hash lookup in most real code. If the default is a mutable object like a list or dict, a new instance is allocated on every call regardless of whether the key exists. When this happens inside a loop over thousands of items, the wasted allocations add up. Using a sentinel or an explicit check can avoid that cost, but it also makes the code longer.
# Avoids constructing a new list when the key exists if key in data: value = data[key] else: value = data[key] = []
This explicit form is equivalent to setdefault but only constructs the list when needed. For most applications the difference is negligible, but it matters in tight loops where allocation pressure is a concern.
Choosing Between get, setdefault, and defaultdict
Use get when you need to read a value and the dictionary must remain unchanged. This is the right choice for lookups in shared state, configuration maps, or any situation where a read should not create entries.
Use setdefault when you need the value immediately and want to store a default only on first access. It is the clearest option when the code naturally wants to "get or create" in a single expression, such as building a grouping structure.
Use collections.defaultdict when the default is the same for every missing key and you want implicit behavior across many operations. A defaultdict(list) automatically creates a new list on any missing-key access, which is convenient but can hide bugs when a missing key is a genuine error.
| Method | Mutates dict | Default evaluated | Best fit |
|---|---|---|---|
get | No | Eagerly | Read-only lookup |
setdefault | Only if missing | Eagerly | Get-or-create in one step |
defaultdict | Yes | On access | Uniform default for many keys |
The decision is not about which method is faster in general. It is about whether the dictionary should change when a key is absent and whether the default value should be created eagerly or lazily.
A Practical Example: Building a Nested Structure
Consider a function that reads a list of records and groups them by department. The setdefault approach is compact and directly expresses the intent.
def group_by_department(records): grouped = {} for record in records: grouped.setdefault(record.department, []).append(record) return grouped
The equivalent using get requires an explicit branch and a separate assignment, which is more verbose but avoids constructing a list when the department already exists.
def group_by_department_get(records): grouped = {} for record in records: if record.department in grouped: grouped[record.department].append(record) else: grouped[record.department] = [record] return grouped
Both functions produce the same result. The setdefault version is shorter and easier to read. The get version avoids the wasted list allocation for existing keys. In a typical grouping workload with a small number of departments and many records, the allocation difference is minor, and readability usually wins. In a tight loop processing millions of records with few repeated keys, the explicit branch may be worth the extra lines.
There is also a subtle correctness difference between the two approaches when the default value is mutable and shared. setdefault(key, []) creates a fresh list each time the key is missing, so two different missing keys never share a list. If you instead wrote default = []; data.setdefault(key, default), every missing key would share the same list object, which is almost never what you want. The eager evaluation of the default argument makes this easy to get wrong, so keep the default construction inside the call or use a factory-based approach like defaultdict when the default needs to be fresh per key.