~/tutorials/python-dictionary-methods-the-11-you-actually-use-with-examples
§ POST · MAY 14, 2026 v1.0

Python dictionary methods: the 11 you actually use, with examples

The 11 Python dict methods you reach for daily: get, setdefault, update, items, pop, the merge operator, plus dict comprehension. With code for each.
Ryan CallowayStaff contributor
  8 min read

By Ryan Calloway. Updated May 2026.

Python dictionaries have exactly 11 methods. Most production code reaches for five of them daily, two of them weekly, and the remaining four only when you really need them. This guide covers all 11 — in the order I reach for them — plus the modern | merge operator, dict comprehensions, and the three mistakes I flag in code review every week. All examples run on Python 3.13.

Quick answer: get for safe reads, setdefault for read-or-insert, update (or the | operator) for merges, pop for read-and-remove, items/keys/values for iteration. copy when you mean shallow copy, clear when you really want to empty in place, popitem when LIFO matters, and fromkeys almost never. The rest is examples.

What you’ll learn

Prerequisites

The 11 methods at a glance

Method Frequency One-line summary
d.get(k, default) Daily Safe read with a default
d.setdefault(k, default) Weekly Read-or-insert in one call
d.update(other) Daily Merge another dict in place
d.pop(k, default) Weekly Read and remove a key
d.popitem() Rare Remove the last-inserted entry (LIFO)
d.keys() Daily Live view of the keys
d.values() Daily Live view of the values
d.items() Daily Live view of (key, value) pairs
d.copy() Occasional Shallow copy
d.clear() Rare Empty the dict in place
dict.fromkeys(it, v) Avoid Build a dict from keys; the mutable-default footgun

1. get — stop writing if key in d

# Verbose
if "port" in config:
    port = config["port"]
else:
    port = 5432

# Idiomatic
port = config.get("port", 5432)

Two gotchas. d.get(k) with no default returns None when the key is missing; if you then index into the result you land on TypeError: 'NoneType' object is not subscriptable. Pass an explicit default. And d.get(k, []) evaluates the default on every call (not lazily); on hot paths with a list or dict default that adds up to real allocations.

2. setdefault — the multi-map pattern

users_by_role: dict[str, list] = {}
for u in users:
    users_by_role.setdefault(u["role"], []).append(u)

On first occurrence of a role, setdefault inserts [] and returns it. On every later occurrence it returns the existing list. The whole “build a dict of lists” pattern in one line.

If every key produces the same default, prefer collections.defaultdict. It removes the explicit setdefault call and signals intent.

from collections import defaultdict
users_by_role: defaultdict[str, list] = defaultdict(list)
for u in users:
    users_by_role[u["role"]].append(u)

For counters, prefer collections.Counter over setdefault(k, 0) + 1. Counter.most_common(n) alone earns the import.

3. update — merge in place

defaults = {"host": "localhost", "port": 5432, "ssl": False}
overrides = {"port": 6432, "ssl": True}

defaults.update(overrides)
# {'host': 'localhost', 'port': 6432, 'ssl': True}

update mutates the left side; the right side wins on collisions. It also accepts an iterable of pairs (d.update([("a", 1), ("b", 2)])) and keyword arguments (d.update(host="example.com")).

For “merge without mutating”, reach for the | operator instead.

config = defaults | overrides     # new dict, neither input changes
defaults |= overrides             # equivalent to defaults.update(overrides)

The PEP 584 operators (| and |=) shipped in Python 3.9 and are now the idiomatic merge for the create-new-dict case. Pick one convention per module — mixing update and | in the same function reads like noise.

For deep merges (nested dicts), neither update nor | is enough; both overwrite rather than merge recursively. Reach for deepmerge or write a 10-line recursive helper.

4. pop — read and remove

def normalize_payload(body: dict) -> tuple[str, str | None, dict]:
    event_type = body.pop("type", "unknown")
    timestamp  = body.pop("ts", None)
    return event_type, timestamp, body

pop removes the key and returns the value. With a default it never raises; without one it raises KeyError on a missing key. This is how I normalize inbound webhook payloads: pop the fields I care about, pass the rest through.

5. popitem — LIFO remove (rarely useful)

cache = {"a": 1, "b": 2, "c": 3}
last = cache.popitem()
# ('c', 3) - the most recently inserted entry

Since Python 3.7, dicts preserve insertion order, and popitem is documented as LIFO. The use case is implementing a small stack-like cache. Outside that, the behaviour is surprising; reach for pop with an explicit key instead.

6, 7, 8. keys, values, items — live views

for k in d.keys():        # equivalent to: for k in d
    ...
for v in d.values():
    ...
for k, v in d.items():
    ...

All three return view objects, not copies. Views are lazy and reflect changes to the underlying dict — which is also why mutating a dict while iterating one of its views raises RuntimeError: dictionary changed size during iteration.

# Wrong
for k in d:
    if not d[k]:
        del d[k]
# RuntimeError: dictionary changed size during iteration

# Right - take a snapshot first
for k in list(d):
    if not d[k]:
        del d[k]

Views also support set operations on dicts that have hashable values: d.keys() & other.keys() gives the intersection, d.items() - other.items() the difference. Useful when comparing two configurations.

9. copy — shallow copy, no surprises

original = {"a": [1, 2], "b": [3, 4]}
shallow  = original.copy()
shallow["a"].append(99)
# original is now {"a": [1, 2, 99], "b": [3, 4]}

d.copy() creates a new dict with the same key/value references. The keys and values are not deep-copied — mutating a list value through one dict mutates it through the other. For independent copies of nested structures, use copy.deepcopy(d); do not hand-roll it.

dict(d) achieves the same shallow copy and is sometimes preferred for clarity in factory functions; d.copy() is the more common spelling. Same speed.

10. clear — empty in place

cache = {"a": 1, "b": 2}
cache.clear()
# cache is now {}

The reason to use clear over cache = {} is that clear mutates the existing object — every other reference to the dict sees the change. cache = {} rebinds the local variable; other modules holding a reference to the old dict still see the old contents. If your dict is shared (a global, a class attribute, an injected dependency), clear is the right call.

11. fromkeys — handle with care

flags = dict.fromkeys(["a", "b", "c"], False)
# {'a': False, 'b': False, 'c': False}

fromkeys is a class method, not an instance method. The footgun: when the default value is mutable, every key shares the same reference.

shared = dict.fromkeys(["a", "b"], [])
shared["a"].append(1)
# {'a': [1], 'b': [1]}  - both keys point at the same list

Always use a dict comprehension when the default is mutable:

independent = {k: [] for k in ["a", "b"]}
independent["a"].append(1)
# {'a': [1], 'b': []}

For immutable defaults (None, integers, strings, tuples), fromkeys is fine. For anything mutable, reach for the dict comprehension.

Bonus: dict comprehensions

by_id    = {u["id"]: u for u in users}                  # index by key
flipped  = {v: k for k, v in d.items()}                 # swap keys / values
non_null = {k: v for k, v in d.items() if v is not None}  # filter
shifted  = {k: v + 1 for k, v in counts.items()}        # transform values

Same syntax as list comprehensions, with key: value in the expression slot. The list comprehension examples cover the patterns that apply to dicts as well — the rules are identical.

Bonus: the walrus operator inside dict ops

# Cache an expensive lookup once per key
seen: dict[str, str] = {}
for url in urls:
    if (host := host_of(url)) and host not in seen:
        seen[host] = first_seen_at(url)

The walrus (:=) shines anywhere you want to read a value once and check it in the same expression. PEP 572 is the rationale.

Common mistakes

  1. dict.fromkeys(keys, []) for a multi-map. Every key shares the same list reference. Use {k: [] for k in keys}.
  2. "x" in d.keys() for membership checks. Identical behaviour to "x" in d, one allocation more expensive on every call. The shorter form is the idiom.
  3. Mutating a dict while iterating it. Take a snapshot with list(d) or list(d.items()) before deleting or assigning.
  4. Using a list as a dict key. Lists are mutable and therefore unhashable. Use a tuple for composite keys: d[(user_id, tenant_id)] = row.
  5. dict(d) when you mean a deep copy. dict(d) and d.copy() are both shallow. For independent nested structures, copy.deepcopy(d).

FAQ

How do I merge two Python dictionaries?

a | b creates a new dict (Python 3.9+). a.update(b) mutates a. {**a, **b} is the older “splat into a literal” form that still works on every supported Python version. Right-hand side wins on key collisions in all three.

Which is faster: d.get(k) or d[k] with try/except?

get wins when the key often misses. d[k] wrapped in try/except KeyError wins when the key usually hits, because the exception path is cheap when never taken. Both are fast enough on Python 3.13 that you should pick on readability.

Are Python dictionaries ordered?

Yes, since Python 3.7, insertion order is part of the language spec. Earlier 3.6 implementations preserved order as an unintended side effect; do not rely on it on 3.6 or older.

What is the difference between items() and iteritems()?

iteritems() was Python 2. In Python 3, items() already returns a lazy view (the role iteritems filled in Python 2), so the old name is gone. If you see iteritems in a codebase, it is either Python 2 or a port that missed a line.

Can I use a list as a dictionary key?

No. Keys must be hashable; lists are mutable and therefore unhashable. Use a tuple for composite keys, or a frozen dataclass for richer composite keys with named fields.

How big can a Python dict get before performance degrades?

CPython’s dict implementation is a hash table with constant-time average lookup. Performance starts to feel different in the millions of entries because of memory pressure and cache effects, not algorithmic complexity. sys.getsizeof(d) tells you the per-instance overhead; for very large maps, __slots__ on a custom class or a dedicated key-value store usually beats a raw dict.

What about OrderedDict and ChainMap?

Since Python 3.7 the regular dict covers the “ordered” case, so collections.OrderedDict is rarely needed in new code; the one feature it still offers that plain dict lacks is move_to_end(). ChainMap is genuinely useful when you want to look up a key across several dicts in priority order without merging them — the canonical example is layered configuration (defaults < file < env < CLI flags).

Sources and further reading

If you are building structured payloads instead of ad-hoc dicts, the dataclasses vs Pydantic v2 guide covers the shape-safety options. For the comprehension patterns that show up around dicts daily, the list comprehension examples cover the recurring shapes.

esc