Dictionaries ship with a handful of methods that go beyond plain d[key] access: safe lookups that won't crash on missing keys, helpers for inserting defaults, ways to remove items while getting their value back, bulk merges, and live views over the keys, values, and items. This lesson covers the methods you'll reach for daily, working through each one in an e-commerce setting where the dictionary is usually a product catalog, a cart with quantities, or a customer profile.
get: Safe Access Without KeyErrorLooking up a key with d[key] raises KeyError when the key isn't there. That's fine when missing keys are a real bug, but most of the time you'd rather get a default and move on. get(key, default=None) does exactly that.
Three things to notice. First, when the key is present, get returns the stored value, no different from prices["Wireless Mouse"]. Second, when the key is absent and you pass no default, get returns None instead of raising. Third, you can hand it any default value you want, and that value is what comes back on a miss.
Compare that with the bracket form:
Same lookup, same missing key, but the bracket form crashes. Pick get when "missing" is a normal case (an optional setting, a product that may or may not be in the catalog, a feature flag that defaults off). Stick with [] when missing genuinely means a bug, because a crash there is more informative than a silent None.
A very common pattern is using get to fold a default into the result on the spot:
Without get, that loop would need an if product in discounts check before every lookup. With get, the default sits right where you use it, and the code reads top to bottom without branches.
get does not insert the default into the dictionary. It only returns it. After the loop above, discounts still has just two keys.
That's an important property. If you want the default to stick (so the next lookup finds it), reach for setdefault.
setdefault: Insert-If-Missing, Then Returnsetdefault(key, default) is the cousin of get that also writes. If the key already exists, it returns the current value and changes nothing. If the key is missing, it inserts key: default into the dictionary and then returns default.
The first call sees "Wireless Mouse" is already there and hands back the existing 2. The second call doesn't find "USB Cable", so it inserts the pair "USB Cable": 1 and returns the freshly inserted 1. After both calls, the dictionary holds both keys.
The reason setdefault is useful is for the "if it's there, use it; if not, start it" pattern. The classic example is grouping records by a key:
For each row, setdefault(customer, []) either fetches the existing list for that customer or creates a new empty list and stores it in the dict. Either way, you get a list back, and you can .append(product) to it right there. Without setdefault, you'd need an explicit if customer not in orders_by_customer: orders_by_customer[customer] = [] ahead of every append.
There's a gotcha worth knowing. The default value is evaluated every time you call setdefault, even when the key already exists.
Both calls printed "computing default...", even though only the second one actually used the new list. Python evaluates expensive_default() before passing it to setdefault. For cheap defaults like 0, "", or [], this doesn't matter. For expensive ones, prefer defaultdict or a manual if key not in d branch.
get vs setdefault vs []: Side by SideThe three are similar enough that beginners often pick the wrong one. This table sorts out the differences.
| Operation | Returns | Inserts on Miss? | Raises on Miss? |
|---|---|---|---|
d[key] | Stored value | No | KeyError |
d.get(key) | Stored value or None | No | No |
d.get(key, default) | Stored value or default | No | No |
d.setdefault(key, default) | Stored value or default | Yes (writes default) | No |
The rule of thumb: use [] when missing is a bug, get when you just want a value to use right now, and setdefault when you want the default to stick for next time.
pop: Remove a Key and Get Its Valuepop(key) deletes the entry and returns the value. If the key isn't there and you didn't pass a default, it raises KeyError.
pop is the one place where "remove and tell me what was there" is a single step. That makes it the right tool when you actually need the value back, not just the side effect of deletion. A classic example: drain a cart item by item and process each removal.
When the key is missing, pop raises by default:
Pass a default and the raise goes away, exactly like get:
The default is what you get back; the dictionary is left alone. A defaulted pop is the cleanest way to write "remove this entry if it exists, otherwise do nothing".
pop and del look related, but they're not interchangeable.
| Operation | Returns | Raises on Miss? | Can Take a Default? |
|---|---|---|---|
d.pop(key) | Removed value | KeyError | Yes |
d.pop(key, default) | Removed value or default | No | Yes |
del d[key] | (statement, no value) | KeyError | No |
Pick pop when you need the value back or want the default fallback. Pick del when you're throwing the value away and a crash on a missing key is appropriate.
popitem: Remove the Last Inserted Pairpopitem() removes one entry from the dictionary and returns it as a (key, value) tuple. Since Python 3.7, dictionaries remember insertion order, and popitem removes from the last inserted end. That makes it a LIFO operation: the most recently added pair comes off first.
The pairs come out in reverse insertion order: HDMI Cable first, then USB Cable. This is useful when you want to undo recent additions or drain a dictionary in reverse order of how it was built.
Cost: popitem is O(1). It pulls from the end of the insertion-ordered structure without scanning anything.
Calling popitem on an empty dictionary raises:
Unlike pop, popitem doesn't accept a default. If there's nothing to pop, it raises, period. Guard with a length check or a try/except if you're draining a dict in a loop.
The while cart check short-circuits to False when the dict is empty, so the loop never calls popitem on an empty container. This is the cleanest drain-in-reverse pattern.
Before Python 3.7, popitem removed an arbitrary pair (it was officially "unspecified"). Modern code can rely on LIFO order, but if you ever see code that calls popitem and doesn't care which pair comes back, the author may have been writing for older Python.
update: Merge Another Dictionary In Placeupdate(other) writes every pair from other into d, overwriting existing keys and inserting new ones. It mutates d in place and returns None.
The USB Cable price got overwritten with the new value. HDMI Cable was new, so it got inserted. Wireless Mouse was untouched because the second dict didn't mention it. That's the rule: existing keys win in the other dict's favor, missing keys come in fresh, unrelated keys stay as they are.
update accepts three shapes of input. A dictionary is the most common, but you can also pass an iterable of (key, value) pairs or keyword arguments.
The keyword form is convenient for small additions where the keys are simple words, but it doesn't work for keys with spaces or special characters. The dict and pair-list forms work for any hashable key.
There's no return value to chain off, so don't write result = prices.update(...); result will be None. The same "in-place methods return None" rule that applies to lists also applies to dicts.
A common use: applying a partial update from a function or external source, like an admin tool that updates a few product prices without re-uploading the whole catalog.
Two prices changed, the other two stayed. One call did the whole merge.
Cost: update is O(k), where k is the number of pairs in the incoming source. Each pair costs a regular dict insert.
Since Python 3.9, the | and |= operators offer another way to merge. d1 | d2 returns a new merged dict, and d1 |= d2 is equivalent to d1.update(d2). Use update when you want in-place mutation, | when you want a fresh dict, and |= when you like operator syntax for the in-place case. They're all the same idea wearing different clothes.
keys, values, items: Dynamic ViewsThese three methods give you a way to iterate the dictionary's contents. They don't return lists; they return view objects that are live windows onto the dictionary.
The repr shows the type (dict_keys, dict_values, dict_items), which is a hint that these aren't ordinary lists. They support iteration and in checks, but they don't support indexing, and they reflect changes to the underlying dictionary as those changes happen.
We never reassigned view. The same view object now reports three keys instead of two, because the dictionary it watches has grown. This is what "dynamic" means here: views are tied to the dict, not snapshots of it.
The relationship between a dict and its three views looks like this:
The dictionary in cyan is the source of truth. Each view (orange, teal, green) is a different projection of the same underlying data. Mutating the dict changes all three views automatically. You don't store the views; you reach for them when you need them and let go when you're done.
The most common use is iteration:
Iterating a dict directly is the same as iterating keys(), so for product in prices: and for product in prices.keys(): are equivalent.
The keys() view is set-like. You can use set operators (&, |, -, ^) on it as if it were a set, which makes comparing dictionaries by their keys clean.
& gives you the keys in both dicts (products sold in both regions). - gives you keys in the first but not the second (US-only products). | gives you the union (every product that appears in either catalog). The items() view is also set-like when every value is hashable, which lets you compare full key-value pairs the same way.
values() is not set-like, because values are allowed to repeat and there's no hashability guarantee. You can iterate it, count things in it, and check membership with in, but you can't do set algebra on it.
clear: Empty the Dictionaryclear() removes every key-value pair. It mutates in place and returns None.
Just like with lists, there's a difference between clearing a dict in place and rebinding the name to a new empty dict. The difference shows up when something else holds a reference to the same dict.
clear empties the underlying dictionary, and because saved points to the same object, it sees the change.
cart_b = {} doesn't empty the existing dict. It rebinds the name cart_b to a brand-new empty dict. The original dict is unchanged, and saved still points to it.
| Form | Mutates the existing dict? | Other references see the change? |
|---|---|---|
d.clear() | Yes | Yes |
d = {} | No (creates a new dict) | No |
The same trap that bites list users bites dict users. If you only have one name pointing to the dict, the two forms look identical. The moment something else holds a reference, the choice matters.
copy: Shallow Copycopy() returns a new dictionary that contains the same key-value pairs as the original. Modifying one dict afterward does not affect the other.
Adding to duplicate didn't touch original. That's the whole point: copy gives you an independent dictionary.
The word "shallow" matters when the values are themselves containers. A shallow copy duplicates the top-level dictionary, but the value objects are shared with the original.
Both dicts show the same list with three prices, because they share the same list object as a value. The shallow copy only duplicated the dict structure, not the nested list inside it.
If you need every nested object to be independent too, use copy.deepcopy from the copy module.
With deepcopy, the nested list is also duplicated, so changes to one side don't leak to the other.
dict.fromkeys: Build a New Dict from Keysdict.fromkeys(iterable, value=None) is a class method (called on the dict type itself, not on an instance) that builds a new dictionary using the elements of iterable as keys, all sharing the same value.
That's a quick way to initialize a counter, a stock map, or any structure where you know the keys up front and want every value to start at the same baseline. With no value supplied, every key gets None.
The gotcha is mutable shared defaults. If the value you pass is mutable (a list, a dict, a set), all the keys share the same object, not separate copies of it.
What's wrong with this code?
Appending to reviews["Wireless Mouse"] reaches the same list every key points to, so the review shows up under every product. fromkeys doesn't copy the value once per key, it stores the same object reference under each key.
Fix (use a comprehension or a loop to give each key its own list):
The dict comprehension {p: [] for p in products} evaluates [] fresh for each iteration, so every product gets a separate list.
The same problem doesn't happen with immutable defaults like numbers, strings, None, or tuples. Those are safe to share because nobody can mutate them.
counters["Wireless Mouse"] += 1 doesn't mutate the integer 0; it rebinds the key to a new integer 1. Integers are immutable, so the shared default is fine. The rule of thumb: fromkeys is safe with None, numbers, strings, and tuples. Be careful with lists, dicts, sets, or anything else mutable, and prefer a comprehension when each key needs its own copy.
Most of these methods show up together in real code. The flow below traces a shopping cart from empty to checkout, with prices coming from a catalog and quantities tracked in the cart itself.
The cart grows with setdefault (to start a quantity counter for a new product) and update (to merge a batch addition). Prices are looked up safely with get. Items leave the cart with pop (taking the value with them) and popitem (peeling off the most recently added pair). Code that matches the diagram:
One snippet, six methods, all on a single cart and catalog. This is the shape most dict-heavy code takes once you've got the methods in your toolkit.
A consolidated table of everything covered in this lesson, for quick lookup when you're deciding which method to reach for.
| Method | Returns | Mutates? | Raises | Notes |
|---|---|---|---|---|
d.get(key, default=None) | Value or default | No | Never | Use when "missing" is normal |
d.setdefault(key, default) | Value or default | Yes (on miss) | Never | Insert-if-missing + return |
d.pop(key) | Removed value | Yes | KeyError on miss | Returns the value, unlike del |
d.pop(key, default) | Removed value or default | Yes (only if present) | Never | Safe remove |
d.popitem() | (key, value) | Yes | KeyError if empty | LIFO since 3.7 |
d.update(other) | None | Yes | Never | Merges dict, pairs, or kwargs |
d.keys() | View | No | Never | Set-like; live |
d.values() | View | No | Never | Not set-like; live |
d.items() | View | No | Never | Set-like if values hashable; live |
d.clear() | None | Yes | Never | Empties in place |
d.copy() | New dict | No | Never | Shallow; nested values shared |
dict.fromkeys(it, value=None) | New dict | No (class method) | Never | All keys share one value object |
Two patterns to notice. First, every mutating method except pop and popitem returns None; those two return the removed data because that's what you actually want from a remove operation. Second, the methods split neatly into "safe" ones (get, setdefault, pop with default) that never raise, and "strict" ones (pop without default, popitem) that do.
10 quizzes