Problem Solved

Python · iterate dictionary

Home › Courses › Python › Problems › Iterate dictionary
🐍 Concept · 3 min read

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.

iterate.py
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.

modify.py
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.

set-ops.py
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.

1. Modifying a dict while iterating

RuntimeError. Fix: collect keys first, or build a new dict.

2. for k, v in d:

Unpacking error — iterating yields keys only. Fix: for k, v in d.items():.

3. d.keys()[0]

Not subscriptable. Fix: list(d.keys())[0].

4. d.items().sort()

No sort method on views. Fix: sorted(d.items()).

5. Assuming insertion order before Python 3.7

Old dicts were unordered. Fix: use OrderedDict or upgrade.

6. for k in d.keys():

Redundant. Fix: for k in d:.

7. Comparing values() as sets

TypeError on unhashable values. Fix: items() or convert manually.

8. Iterating in sorted order without sorting

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

🗺️ Python learning path
The full roadmap from install to first project — every concept in order

Next in this series:

⚖️ Ternary operator
a if condition else b — Python's one-line conditional
← All Python problems
Problem solved · Python