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(), the del statement
  • 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 computes hash(key), uses it to locate a bucket in the internal table, and returns the value stored there — or raises KeyError if 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(), and items() 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 in list(...) 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 (or collections.Counter for heavier counting work) instead of manual if key in dict checks.
  • 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 of for 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 over list(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() or setdefault() (not collections.Counter).
  • Exercise 3: Given two dictionaries, base = {"a": 1, "b": 2} and extra = {"b": 20, "c": 3}, produce a merged dictionary where extra‘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 raises KeyError on a missing key.
  • keys(), values(), and items() return live views, not static lists.
  • update(), |, and |= merge dictionaries, with the incoming/right-hand values winning on key conflicts.
  • pop(), popitem(), del, and clear() 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; use copy.deepcopy() for fully independent nested structures.