Python Dictionary Methods
A Python dict is a mutable collection of key-value pairs, and its power comes not just from square-bracket access but from a rich set of built-in methods for reading, updating, merging, and cleaning up data safely. Instead of writing manual loops and if checks to avoid crashes, you can lean on methods like get(), pop(), and setdefault() to handle missing keys gracefully. This lesson covers every commonly used dictionary method in depth, how it behaves internally, and the mistakes that trip up even experienced developers.
Overview / How it works
Under the hood, a Python dictionary is a hash table. Each key is passed through a hash function to compute a slot in an internal array, and the corresponding value is stored at that slot. This is why dictionary lookups, insertions, and deletions are, on average, O(1) — constant time — regardless of how many items the dictionary holds, unlike a list where searching for a value requires scanning every element.
Because keys are hashed, they must be hashable and effectively immutable — strings, numbers, and tuples (of hashable items) can be keys, but lists, sets, and other dictionaries cannot. Since Python 3.7, dictionaries also maintain insertion order as a language guarantee: iterating over a dict’s keys, values, or items always returns them in the order they were added, even though the underlying storage is hash-based. This ordering is exactly what you’ll see reflected in the example outputs below.
Dictionary methods generally fall into four groups:
- Reading safely:
get(),keys(),values(),items() - Adding or updating:
update(),setdefault(), the merge operators|and|= - Removing:
pop(),popitem(),clear(), thedelstatement - Creating/copying:
dict.fromkeys(),copy()
Syntax
value = my_dict.get(key, default=None)
my_dict[key] = value
my_dict.update(other_dict_or_iterable)
removed = my_dict.pop(key, default)
last_pair = my_dict.popitem()
value = my_dict.setdefault(key, default)
new_dict = dict.fromkeys(iterable, value)
copy_of_dict = my_dict.copy()
my_dict.clear()
| Method | Purpose | Raises on missing key? |
|---|---|---|
get(key, default) |
Read a value; returns default (or None) if absent |
No |
keys() |
Returns a view of all keys | — |
values() |
Returns a view of all values | — |
items() |
Returns a view of (key, value) tuples | — |
update(other) |
Merges another dict/iterable in, overwriting existing keys | No |
pop(key, default) |
Removes and returns a key’s value | Yes, unless default given |
popitem() |
Removes and returns the last-inserted (key, value) pair | Yes, if dict is empty |
setdefault(key, default) |
Returns value if key exists, else inserts default and returns it |
No |
dict.fromkeys(iter, value) |
Builds a new dict from an iterable of keys, all mapped to the same value | — |
copy() |
Returns a shallow copy of the dict | — |
clear() |
Removes all items in place | No |
Examples
Example 1: Reading with get(), keys(), values(), items(), and updating
student = {"name": "Ravi", "age": 22, "course": "Python"}
print(student.get("name"))
print(student.get("grade"))
print(student.get("grade", "Not Assigned"))
print(list(student.keys()))
print(list(student.values()))
print(list(student.items()))
student.update({"age": 23, "grade": "A"})
print(student)
Output:
Ravi
None
Not Assigned
['name', 'age', 'course']
['Ravi', 22, 'Python']
[('name', 'Ravi'), ('age', 22), ('course', 'Python')]
{'name': 'Ravi', 'age': 23, 'course': 'Python', 'grade': 'A'}
get("grade") returns None instead of raising a KeyError, because "grade" isn’t in the dict yet and no default was supplied. Passing an explicit default ("Not Assigned") avoids the awkward None. Note that update() both overwrites the existing "age" key and appends the brand-new "grade" key at the end — insertion order is preserved for old keys and new keys are appended.
Example 2: Removing items with pop(), popitem(), and setdefault()
inventory = {"apples": 10, "bananas": 5, "cherries": 20}
removed_value = inventory.pop("bananas")
print(f"Removed: {removed_value}")
print(inventory)
last_item = inventory.popitem()
print(f"Popped last item: {last_item}")
print(inventory)
count = inventory.setdefault("apples", 0)
print(f"Apples: {count}")
new_count = inventory.setdefault("dates", 15)
print(f"Dates: {new_count}")
print(inventory)
try:
inventory.pop("mangoes")
except KeyError as e:
print(f"Error: {e}")
safe_value = inventory.pop("mangoes", "not found")
print(safe_value)
Output:
Removed: 5
{'apples': 10, 'cherries': 20}
Popped last item: ('cherries', 20)
{'apples': 10}
Apples: 10
Dates: 15
{'apples': 10, 'dates': 15}
Error: 'mangoes'
not found
pop("bananas") deletes that entry and returns its value. popitem() removes whichever pair was inserted last (here, "cherries") — it’s a LIFO operation, useful for treating a dict like a stack. setdefault("apples", 0) does nothing because "apples" already exists — it just returns the current value; but setdefault("dates", 15) both inserts the key and returns the new value, in one step. Calling pop() on a missing key without a default raises KeyError, while supplying a default avoids the exception entirely.
Example 3: A realistic word counter plus merging dictionaries
text = "the quick brown fox jumps over the lazy dog the fox runs"
words = text.split()
frequency = {}
for word in words:
frequency[word] = frequency.get(word, 0) + 1
print(frequency)
vowel_counts = dict.fromkeys("aeiou", 0)
for word in words:
for char in word:
if char in vowel_counts:
vowel_counts[char] += 1
print(vowel_counts)
defaults = {"volume": 50, "brightness": 70}
overrides = {"brightness": 90, "contrast": 60}
settings = defaults | overrides
print(settings)
defaults |= overrides
print(defaults)
Output:
{'the': 3, 'quick': 1, 'brown': 1, 'fox': 2, 'jumps': 1, 'over': 1, 'lazy': 1, 'dog': 1, 'runs': 1}
{'a': 1, 'e': 4, 'i': 1, 'o': 5, 'u': 3}
{'volume': 50, 'brightness': 90, 'contrast': 60}
{'volume': 50, 'brightness': 90, 'contrast': 60}
The classic frequency[word] = frequency.get(word, 0) + 1 pattern is the idiomatic way to build a counting dictionary without checking if word in frequency first. dict.fromkeys("aeiou", 0) quickly builds a dictionary with five keys all initialized to 0. The | operator (Python 3.9+) merges two dicts into a brand-new one without touching either original, with the right-hand dict’s values winning on key conflicts ("brightness" becomes 90). The in-place version, |=, does the same merge but modifies defaults directly instead of creating a new dict.
Under the hood: step by step
- When you write
my_dict[key], Python computeshash(key), uses it to locate a bucket in the internal table, and returns the value stored there — or raisesKeyErrorif the bucket is empty for that exact key. get()performs the same lookup but catches the “not found” case internally and returns a default instead of propagating an exception — this is why it’s considered the “safe” version of[]access.keys(),values(), anditems()don’t return lists — they return special view objects that stay live: if the dictionary changes afterward, the view reflects the change automatically. Wrap them inlist(...)when you need a frozen snapshot.update()and the merge operators iterate over the incoming dictionary (or iterable of key-value pairs) and, for each key, either insert it fresh or overwrite the existing value — insertion order for pre-existing keys never changes, only their value does.copy()produces a shallow copy: a new dictionary object with the same top-level keys and values. If a value is itself a mutable object (like a nested list), both the original and the copy point to the same nested object.
Common Mistakes
Mistake 1: Using square brackets on a possibly-missing key
Writing counts[word] += 1 before word has ever been added raises KeyError, because += first tries to read the current value. The fix is to seed the key with get() or setdefault():
counts = {}
counts["apple"] += 1 # KeyError: 'apple'
counts = {}
counts["apple"] = counts.get("apple", 0) + 1
print(counts)
Mistake 2: Assuming copy() deep-copies nested structures
A shallow copy() shares nested mutable objects with the original, so mutating a nested list through the copy also changes the original:
original = {"tags": ["python", "dict"]}
shallow = original.copy()
shallow["tags"].append("bug")
print(original["tags"]) # ['python', 'dict', 'bug'] — the original changed too!
Use copy.deepcopy(original) from the copy module when the values themselves contain mutable collections that must be fully independent.
Mistake 3: Calling pop() or popitem() without handling the missing/empty case
pop(key) without a default raises KeyError if the key is absent, and popitem() raises KeyError on an empty dict. Always supply a default to pop(), or check length/use a try/except, when the key’s presence isn’t guaranteed.
Best Practices
- Prefer
get(key, default)over[]access whenever a key might legitimately be missing. - Use
word_count[word] = word_count.get(word, 0) + 1(orcollections.Counterfor heavier counting work) instead of manualif key in dictchecks. - Use
setdefault()when you need to both check-and-insert a default and immediately use the resulting value — for example, building groups:groups.setdefault(category, []).append(item). - Use the
|operator for merging two dicts into a new one, and|=for merging in place — both are clearer than{**a, **b}unpacking, though that syntax still works on older Python versions. - Iterate with
.items()instead offor key in d: value = d[key]— it’s a single lookup instead of a hash-and-fetch per iteration. - Use
copy.deepcopy(), not.copy(), whenever a dictionary’s values are themselves mutable collections you intend to modify independently. - Avoid mutating a dictionary (adding/removing keys) while iterating directly over it — this raises
RuntimeError. Iterate overlist(d.items())instead if mutation is required mid-loop.
Practice Exercises
- Exercise 1: Given
prices = {"pen": 10, "notebook": 40, "eraser": 5}, write code that prints the price of"pencil"if it exists, or"Not sold here"otherwise, without raising an exception. - Exercise 2: Write a function that takes a list of words and returns a dictionary mapping each unique word to how many times it appears, using
get()orsetdefault()(notcollections.Counter). - Exercise 3: Given two dictionaries,
base = {"a": 1, "b": 2}andextra = {"b": 20, "c": 3}, produce a merged dictionary whereextra‘s values win on conflicts, using the|operator. What is the expected output?
Summary
- Dictionaries are hash tables offering average O(1) lookup, insertion, and deletion, while preserving insertion order since Python 3.7.
get()reads safely with an optional default; plain[]access raisesKeyErroron a missing key.keys(),values(), anditems()return live views, not static lists.update(),|, and|=merge dictionaries, with the incoming/right-hand values winning on key conflicts.pop(),popitem(),del, andclear()remove items — supply defaults or use exception handling when the key might not exist.setdefault()inserts-if-missing and returns the value in a single call, ideal for building grouped data structures.copy()is shallow; usecopy.deepcopy()for fully independent nested structures.
