How to iterate a dict.
keys, values, or items.
Dicts are everywhere in Python — configs, caches, JSON data. To work with them, you need to iterate them. Three views: .keys(), .values(), .items(). Each returns a dynamic, lazy view — not a list. The most common mistake is modifying the dict while iterating (raises RuntimeError). The superpower most people miss: .keys() and .items() support set operations.
Short answer: for k in d: or for k in d.keys(): — keys. for v in d.values(): — values. for k, v in d.items(): — both. All views are lazy and reflect live changes. Don't modify the dict while iterating — collect keys first, then modify. Python 3.7+ preserves insertion order. Set operations work on keys: d1.keys() & d2.keys() gives common keys.
✅ The three views
Keys, values, or both.
d = {'name': 'Alice', 'age': 30, 'city': 'Lahore'}
# Iterate keys — two equivalent ways
for k in d:
print(k)
# name
# age
# city
for k in d.keys():
print(k)
# same output
# Iterate values
for v in d.values():
print(v)
# Alice
# 30
# Lahore
# Iterate items (key-value pairs) — most common
for k, v in d.items():
print(f'{k}: {v}')
# name: Alice
# age: 30
# city: Lahore
# All three views are lazy — no list is built
print(type(d.keys())) #
print(type(d.values())) #
print(type(d.items())) #
# Convert to list if needed
print(list(d.keys()))
# ['name', 'age', 'city']
# Views reflect live changes
keys = d.keys()
d['email'] = 'alice@example.com'
print(list(keys))
# ['name', 'age', 'city', 'email'] — 'email' appears
What each returns:
d.keys() → dict_keys view
yields keys
supports set operations (a & b, a | b, ...)
reflects live changes
d.values() → dict_values view
yields values
NO set operations (values can be unhashable)
reflects live changes
d.items() → dict_items view
yields (key, value) tuples
supports set operations (on the tuples)
reflects live changes
What each view supports:
keys() values() items()
----- ------ ------
len yes yes yes
in yes yes yes
iter yes yes yes
list() yes yes yes
& yes no yes
| yes no yes
- yes no yes
^ yes no yes
Why values() has no set operations:
Values can be unhashable (lists, dicts).
Set operations require hashable elements.
Keys and items (if keys are hashable)
satisfy the constraint.
So values() can't support set operations —
not every value is hashable.
Why views are not lists:
❌ d.keys()[0] # TypeError — not subscriptable
❌ d.keys().sort() # AttributeError — no sort method
✅ list(d.keys())[0] # convert first
✅ sorted(d.keys()) # sort by passing to sorted()
✅ sorted(d.items(), key=lambda x: x[1]) # sort items
Views are lazy — no list allocation, no memory overhead.
For most code, that's exactly what you want.
Dict iteration order:
Python 3.7+: insertion order.
d = {'b': 1, 'a': 2}
list(d) # ['b', 'a'] (not alphabetical, not sorted)
To sort by key:
for k in sorted(d):
To sort by value:
for k, v in sorted(d.items(), key=lambda x: x[1]):
The default iteration (unsorted) is fast — O(n).
Sorted iteration is O(n log n).
For plain iteration where order doesn't matter,
just iterate. For display or output, sort.
✅ The modification trap
What not to do, and the safe pattern.
d = {'a': 1, 'b': 2, 'c': 3}
# ❌ Modifying during iteration
try:
for k in d:
if d[k] < 2:
del d[k]
except RuntimeError as e:
print(e) # dictionary changed size during iteration
# ✅ Safe pattern — collect, then modify
to_remove = [k for k in d if d[k] < 2]
for k in to_remove:
del d[k]
print(d) # {'b': 2, 'c': 3}
# ✅ Or iterate over a copy
d = {'a': 1, 'b': 2, 'c': 3}
for k in list(d):
if d[k] < 2:
del d[k]
print(d) # {'b': 2, 'c': 3}
# ✅ Or use dict comprehension (creates new dict)
d = {'a': 1, 'b': 2, 'c': 3}
d = {k: v for k, v in d.items() if v >= 2}
print(d) # {'b': 2, 'c': 3}
# Modifying values is fine — only size changes fail
d = {'a': 1, 'b': 2}
for k in d:
d[k] = d[k] * 10
print(d) # {'a': 10, 'b': 20} ✅ works
# Adding new keys during iteration fails too
d = {'a': 1}
try:
for k in d:
d['new_' + k] = 0
except RuntimeError as e:
print(e) # dictionary changed size during iteration
The rule:
You can MODIFY VALUES during iteration:
for k in d:
d[k] = d[k] * 2 ✅ works
You CANNOT change SIZE during iteration:
for k in d:
d[k + '_new'] = 0 ❌ RuntimeError
del d[k] ❌ RuntimeError
d.clear() ❌ RuntimeError
Why the restriction:
The dict iterator traverses the underlying hash table.
Changing the size changes the hash table —
which invalidates the iterator's position.
Python can't safely continue.
Same logic as modifying a list while iterating:
for i in range(len(lst)):
del lst[i] # breaks indexing
The safe patterns:
1. Collect keys, then modify
to_remove = [k for k in d if should_remove(k)]
for k in to_remove:
del d[k]
✅ Iterate the collected list (not the dict)
✅ Dict is untouched during iteration
✅ Modification happens after
2. Iterate a copy
for k in list(d):
del d[k]
✅ list(d) creates a snapshot
✅ Iterate the list, modify the dict
✅ Slightly more memory (a list of keys)
✅ Works but less explicit than (1)
3. Build a new dict
d = {k: v for k, v in d.items() if not should_remove(k)}
✅ No mutation at all
✅ New dict, old one is garbage collected
✅ Fastest for "remove all matching" operations
✅ Idiomatic Python
4. Iterate keys from a different dict
for k in other_dict:
del d[k]
✅ Not the same dict
✅ No conflict
Which pattern to use:
✅ (3) for removing items based on value:
d = {k: v for k, v in d.items() if v >= 0}
✅ (1) for removing items based on a condition
that involves calling a function or side effects:
to_remove = [k for k in d if should_remove(k)]
for k in to_remove:
del d[k]
✅ (2) for simple in-place modifications:
for k in list(d):
if some_condition(k):
d[k] = new_value(d[k])
The pattern:
If you need to CHANGE SIZE, don't iterate the dict.
Either iterate a copy, or build a new dict.
The RuntimeError is actually helpful:
dict changed size during iteration
It tells you exactly what's wrong.
Without it, you'd get silent corruption —
skipped items, duplicated items, or crashes
much later.
The error is a feature.
Same pattern in Java, C#, and other languages:
modifying a collection while iterating it
is unsafe.
Python catches it early.
In C, you'd get undefined behavior.
Python is kinder.
✅ The set-operation superpower
keys() and items() work like sets.
d1 = {'a': 1, 'b': 2, 'c': 3}
d2 = {'b': 20, 'c': 30, 'd': 40}
# Common keys — in both dicts
common = d1.keys() & d2.keys()
print(common) # {'b', 'c'}
# All keys — in either dict
all_keys = d1.keys() | d2.keys()
print(all_keys) # {'a', 'b', 'c', 'd'}
# Only in d1 — not in d2
only_d1 = d1.keys() - d2.keys()
print(only_d1) # {'a'}
# In exactly one — symmetric difference
symmetric = d1.keys() ^ d2.keys()
print(symmetric) # {'a', 'd'}
# Compare items too
common_items = d1.items() & d2.items()
print(common_items) # set() — no (key, value) pair is identical
# Real-world: find conflicting config keys
defaults = {'host': 'localhost', 'port': 8080, 'debug': False}
user = {'port': 9090, 'debug': True}
conflicts = defaults.keys() & user.keys()
print(conflicts) # {'port', 'debug'}
# Real-world: find missing required keys
required = {'name', 'email', 'age'}
provided = {'name', 'email'}
missing = required - provided
print(missing) # {'age'}
# Real-world: user has these permissions,
# admin has these — which are common?
user_perms = {'read', 'write', 'delete'}
admin_perms = {'read', 'write', 'delete', 'admin'}
common_perms = user_perms & admin_perms
print(common_perms) # {'read', 'write', 'delete'}
extra_for_user = user_perms - admin_perms
print(extra_for_user) # set()
only_admin = admin_perms - user_perms
print(only_admin) # {'admin'}
The set operations available on .keys() and .items():
& intersection — in both
| union — in either
- difference — in first but not second
^ symmetric difference — in exactly one
Also: isdisjoint, issubset, issuperset
(as methods)
These work because dict keys must be hashable.
Hashable = can be in a set.
So keys() returns something set-compatible.
What this enables:
1. Compare two configs
diffs = config1.keys() ^ config2.keys()
# keys that differ
2. Check required keys
missing = required - provided
if missing:
raise ValueError(f'Missing: {missing}')
3. Find conflicts
conflicts = defaults.keys() & user.keys()
# keys present in both
4. Filter by allowed keys
allowed = {'name', 'age', 'email'}
filtered = {k: d[k] for k in d.keys() & allowed}
5. Compute permissions
effective = user_perms | group_perms
# union of all permissions
6. Find stale keys
stale = old_config.keys() - new_config.keys()
# keys that were removed
The .items() set operations:
(k, v) pairs are hashable IF the key is hashable
AND the value is hashable.
Example:
d1 = {'a': 1}
d2 = {'a': 1}
d1.items() & d2.items() # {('a', 1)}
# Common (key, value) pairs
For dicts with unhashable values (lists, dicts),
items() can't do set operations.
d1 = {'a': [1, 2]}
d2 = {'a': [1, 2]}
d1.items() & d2.items() # TypeError
The values are lists — unhashable.
Keys alone still work:
d1.keys() & d2.keys() # {'a'}
The .values() exception:
values() does NOT support set operations.
Same reason: values can be unhashable.
If you need to compare values as a set,
convert to a set manually:
set(d1.values()) & set(d2.values())
But this only works if all values are hashable.
And it loses the association with keys.
In practice, you rarely need to compare values
without keys. Use items() if you need both.
Don't use .keys() when you don't need it. for k in d.keys() is redundant — for k in d does the same thing. The idiom is to iterate the dict directly. .keys() is needed when you want the view object itself — for set operations (d.keys() & other.keys()), for len(d.keys()) (which is the same as len(d)), or for passing to a function that expects a view. For plain iteration, skip .keys(). Same for if k in d.keys() — write if k in d.
⚠️ Common mistakes
The dict-iteration mistakes that cost real time.
RuntimeError. Fix: collect keys first, or build a new dict.
for k, v in d:Unpacking error — iterating yields keys only. Fix: for k, v in d.items():.
d.keys()[0]Not subscriptable. Fix: list(d.keys())[0].
d.items().sort()No sort method on views. Fix: sorted(d.items()).
Old dicts were unordered. Fix: use OrderedDict or upgrade.
for k in d.keys():Redundant. Fix: for k in d:.
values() as setsTypeError on unhashable values. Fix: items() or convert manually.
Dict order is insertion order, not sorted. Fix: for k in sorted(d):.
❓ Frequently asked
How do I iterate over a dictionary in Python?
Three ways. (1) Iterate keys: for k in d: or for k in d.keys(): — iterating a dict yields keys. (2) Iterate values: for v in d.values(): — yields values only. (3) Iterate key-value pairs: for k, v in d.items(): — yields (key, value) tuples. The third is the most common when you need both.
What is the difference between keys(), values(), and items()?
All three return view objects (not lists) that reflect the current state of the dict. .keys() yields keys, .values() yields values, .items() yields (key, value) tuples. The views are dynamic and lazy. Convert to list if needed: list(d.keys()). Since Python 3.7, the return order is insertion order.
Why does iterating a dict give me keys, not items?
Because that's how dict.__iter__ is defined — it yields keys. The reasoning: iterating a dict should give you the 'identifiers' of the entries. This is consistent with how sets work. To get items, use .items(). To get values, use .values().
Can I modify a dict while iterating?
No — you'll get RuntimeError: dictionary changed size during iteration. The fix: iterate over a copy (for k in list(d):), or collect the keys to remove/modify first. Modifying values is fine (d[k] = new_value) — only size changes are prohibited.
What is the set-operation superpower of .keys()?
The keys() and items() views support set operations: &, |, -, ^. Example: d1.keys() & d2.keys() gives the common keys. d1.keys() | d2.keys() gives all keys. d1.keys() - d2.keys() gives keys only in d1. This works because dict keys must be hashable.
What order are dict items returned in?
In Python 3.7+, insertion order. Before that, it was implementation-defined. The dict preserves the order in which keys were added. Updating an existing key doesn't change its position. Deleting and re-adding a key moves it to the end. For sorted iteration: for k in sorted(d):
🔗 Continue learning
Next in this series:
a if condition else b — Python's one-line conditional