To iterate a dictionary in Python you have four patterns: for key in my_dict (keys, the default), for key in my_dict.keys() (the same, explicit), for value in my_dict.values() (values only), and for key, value in my_dict.items() (pairs). What most tutorials miss: the objects returned by .keys(), .values(), and .items() are not lists. They're view objects that stay in sync with the source dict, and two of them (.keys() and .items()) behave like sets, so you can intersect, union, and subtract them directly. Iteration order has been a language guarantee since Python 3.7, and reversed() works on dicts from 3.8 onward. The classic trap, mutating a dict during iteration, has three clean fixes that we'll walk through below. This piece is one of 17 short explainers in our Python Concepts Explained reference.

Key Takeaways

  • Four patterns: for k in d (keys), d.keys(), d.values(), d.items(). Use .items() whenever you need both halves; tuple-unpack in the loop header for clarity.
  • View objects, not lists. dict_keys, dict_values, and dict_items are dynamic windows onto the dict. Mutate the dict, the view reflects the change immediately. Call list(...) only when you need a frozen snapshot.
  • Set operations on .keys() and .items(). d1.keys() & d2.keys() returns the common keys; -, |, and ^ work too. .values() doesn't support these because values aren't required to be hashable.
  • Insertion order is guaranteed. Since Python 3.7 (PEP 468), iteration follows insertion order. From 3.8 onward, reversed(d) walks the keys backwards.
  • Mutation-during-iteration raises RuntimeError. Three fixes: iterate a list copy (list(d.items())), collect keys-to-delete first and remove after the loop, or rebuild via dict comprehension. Mutating existing values is safe; only changing the key set trips the trap.
The four dictionary iteration patterns and their view objects One dict, four iteration patterns, three view objects my_dict {"a": 1, "b": 2, "c": 3} views stay synced for k in my_dict yields keys 'a', 'b', 'c' default form my_dict.keys() dict_keys {'a', 'b', 'c'} set ops ✓ my_dict.values() dict_values [1, 2, 3] no set ops my_dict.items() dict_items [('a',1),('b',2),('c',3)] set ops ✓ Use in a for loop: for k in d:   print(k) for k in d.keys():   print(k) for v in d.values():   print(v) for k, v in   d.items():     ... Two facts most tutorials miss: 1. Views are LIVE: v = d.keys(); d['x']=99; 'x' in v → True. 2. d1.keys() & d2.keys() returns the common keys. |, -, ^ work the same way. Both facts come from the view-object design, and both unlock patterns most code doesn't use.
One source dictionary, four iteration patterns, three view-object types. Two of those view types support set operations directly.

The Four Iteration Patterns

Every dictionary iteration boils down to one of four shapes. Knowing which to reach for is half the battle.

1. Direct iteration over keys

scores = {"alice": 95, "bob": 80, "charlie": 70}

for name in scores:
    print(name)
# alice
# bob
# charlie
Iterating a dict yields its keys. This is the default and the most common pattern in real code. Reach for it when you only need to know which keys exist.

2. Explicit .keys()

for name in scores.keys():
    print(name)
Functionally identical to direct iteration, slightly more verbose. Use it when the explicit form makes the intent clearer, especially in shared code where a reader benefits from seeing "this loops the keys" spelled out.

3. .values() for values-only iteration

total = 0
for score in scores.values():
    total += score
print(total)   # 245
Use this whenever the keys are uninteresting and only the values matter: summing, finding a min/max, checking membership of a value.

4. .items() for pairs

for name, score in scores.items():
    print(f"{name}: {score}")
# alice: 95
# bob: 80
# charlie: 70
The workhorse pattern. .items() yields (key, value) tuples; Python unpacks them into the two loop variables. Whenever both halves of a pair are useful in the loop body, reach for this. (For the underlying tuple-unpacking mechanic, see our enumerate and zip explainer.)

View Objects Are Not Lists

This single fact resolves a surprising number of dictionary mysteries. The objects .keys(), .values(), and .items() return are view objects, named dict_keys, dict_values, and dict_items. They are not lists; they are dynamic windows onto the underlying dictionary. Mutate the dictionary, and the view immediately reflects the change.
scores = {"alice": 95, "bob": 80}
names = scores.keys()
print(names)   # dict_keys(['alice', 'bob'])

scores["charlie"] = 70
print(names)   # dict_keys(['alice', 'bob', 'charlie'])
# We never reassigned 'names'; the view updated itself.
The view is a live read of the source. This makes them memory-cheap (no copying on every call) and a little surprising if you expect list semantics. Two consequences worth remembering:
  • Indexing doesn't work: scores.keys()[0] raises TypeError. Views aren't indexable. If you need an indexable snapshot, call list(scores.keys()).
  • They can be reused: unlike a generator, a view can be iterated as many times as you want. Each iteration sees the current state of the dict.
If you need a frozen snapshot (for example, to keep the original list of keys around while the dict mutates), list(scores.keys()) or list(scores) is the idiom. Otherwise, prefer the bare view; it's cheaper and reads cleaner.

Set Operations on .keys() and .items()

The most powerful, and the least-used, feature of dict views. Because keys are unique and hashable, dict_keys behaves like a set. So does dict_items (the tuples are hashable as long as the values are). Both support the full set-operator vocabulary:
OperatorMeaningExample output
&intersection (keys in both)shared keys
|union (keys in either)combined key set
-difference (keys in first only)missing-from-other keys
^symmetric difference (in exactly one)keys unique to one side
Three real use cases that come up constantly in production code.

Use case 1: find shared keys between two configs

defaults  = {"host": "localhost", "port": 8080, "ssl": False}
overrides = {"port": 9090, "ssl": True, "debug": True}

common = defaults.keys() & overrides.keys()
print(common)   # {'port', 'ssl'}
Two lines, no loop. Without set operations, you'd write a list comprehension and a not in check. With them, the intent is loud.

Use case 2: validate required keys

required = {"name", "email", "password"}
submitted = {"name": "Ada", "email": "ada@example.com"}

missing = required - submitted.keys()
if missing:
    raise ValueError(f"missing required fields: {missing}")
# ValueError: missing required fields: {'password'}
Subtracting a view from a set gives you everything the dict didn't supply. This is the canonical form for input validation in API code.

Use case 3: find orphan keys (overrides that don't match defaults)

orphans = overrides.keys() - defaults.keys()
print(orphans)   # {'debug'}
Useful for catching typos in config files: any key in the user override that isn't a real setting in the defaults is probably a typo. One line of code; reads like English.

Use case 4: diff two snapshots

before = {"alice": 95, "bob": 80, "charlie": 70}
after  = {"alice": 95, "bob": 85, "diana": 60}

added   = after.keys() - before.keys()      # {'diana'}
removed = before.keys() - after.keys()      # {'charlie'}
both    = before.keys() & after.keys()      # {'alice', 'bob'}

changed = {k for k in both if before[k] != after[k]}
print(changed)   # {'bob'}
Classic data-comparison pattern: which records appeared, which disappeared, which changed. The set operators handle the first three cases on one line each; the value-comparison step uses a set comprehension because .values() can't help. This kind of diff lives at the heart of database sync, cache invalidation, and config-management tooling.

Why .values() doesn't support set operations

Values aren't required to be unique, and they aren't required to be hashable. A dict can have ten keys all mapping to the value [1, 2, 3] (a list, unhashable). Set operations need both uniqueness and hashability, so the language designers refused to pretend they apply to values. If you need set-like behavior on values, wrap them: set(my_dict.values()) drops duplicates and ignores unhashable entries via TypeError.

Insertion Order (3.7+) and reversed() (3.8+)

Until Python 3.6, iteration order was undefined. You could rely on no particular order, and the same dict literal might iterate in different orders on different runs. CPython 3.6 changed the implementation to preserve insertion order, but the language spec didn't promise it. Python 3.7 made it a language guarantee (PEP 468). From 3.7 onward, every Python implementation must iterate dicts in insertion order:
order = {}
order["first"]  = 1
order["second"] = 2
order["third"]  = 3

for key in order:
    print(key)
# first
# second
# third      (every time, every implementation)
Python 3.8 added reversed() support for dicts and dict views (PEP 595):
for key in reversed(order):
    print(key)
# third
# second
# first
For older code that needs the order guarantee on Python 3.6 or earlier, collections.OrderedDict still exists and still works. In modern code, plain dict covers the same ground with less ceremony. The only reason to use OrderedDict now is its specific extra methods: move_to_end() and popitem(last=False).

Practical implications of guaranteed order

The order guarantee shapes more downstream behavior than most people realize. JSON serialization (json.dumps) preserves dict order, so the same dict produces the same JSON output every time, byte-for-byte. That makes JSON-based caching and content hashing reliable in a way it never was before 3.7. The same goes for **kwargs: a function defined as def fn(**kwargs) receives its keyword arguments in the order the caller passed them, which lets debugging output and audit logs trust the sequence. And dict-based config files (TOML, YAML, JSON) round-trip through Python without scrambling key order, which matters whenever a human reads the output.

Mutation During Iteration: The Trap and Three Fixes

The single most common dict-iteration bug: adding or removing keys while iterating raises RuntimeError: dictionary changed size during iteration. Python detects the change via a size counter and refuses to continue.
scores = {"alice": 95, "bob": 80, "charlie": 50, "diana": 30}

for name, score in scores.items():
    if score < 60:
        del scores[name]
# RuntimeError: dictionary changed size during iteration
Three clean fixes, ordered by how often they're the right choice.

Fix 1: iterate over a list snapshot

for name, score in list(scores.items()):
    if score < 60:
        del scores[name]
list(scores.items()) materializes the pairs into a plain list before the loop starts. The dict can then mutate freely; the loop iterates the frozen snapshot. Cheapest cognitive overhead, slightly more memory. Reach for this when the dict is small and you want maximum readability.

Fix 2: collect keys to delete, then delete after the loop

to_delete = [name for name, score in scores.items() if score < 60]
for name in to_delete:
    del scores[name]
Two passes: identify, then remove. More verbose, but it scales better for large dicts because the intermediate list only holds keys to delete, not every key-value pair. Use this when the deletion criterion is expensive to compute or when you want the intent explicit.

Fix 3: rebuild via dict comprehension

scores = {name: score for name, score in scores.items() if score >= 60}
Build a new dict instead of mutating the old one. Often the cleanest option, especially when the result is a filtered or transformed version of the input. The original is replaced wholesale. Use this when you don't need to preserve the dict identity (no other references hold the old dict).
What's allowed: mutating existing values during iteration is safe. for k in d: d[k] = d[k] * 2 works fine. Only adding or removing keys (changing the dict's size) trips the trap.

Sorted Iteration

Dictionaries iterate in insertion order, not sorted order. To walk a dict in a sorted view, pass it through sorted():
scores = {"charlie": 70, "alice": 95, "bob": 80}

# Sorted by key
for name in sorted(scores):
    print(name, scores[name])
# alice 95
# bob 80
# charlie 70

# Sorted by value
for name, score in sorted(scores.items(), key=lambda kv: kv[1]):
    print(name, score)
# charlie 70
# bob 80
# alice 95

# Top 2 by value, descending
top = sorted(scores.items(), key=lambda kv: kv[1], reverse=True)[:2]
print(top)
# [('alice', 95), ('bob', 80)]
For the "top N by frequency" pattern specifically, collections.Counter has a built-in most_common(n) method that does exactly this without the lambda. Use Counter when the dict is genuinely counting things; use sorted() with a key function for everything else.

From Iteration to Construction: The dict() Round-Trip

The inverse of iterating a dict's items is constructing a dict from items. dict() accepts any iterable of key-value pairs, which makes round-tripping and inter-dict transformations trivial.
d = {"a": 1, "b": 2, "c": 3}

# Items round-trips to dict
d2 = dict(d.items())
print(d2 == d)   # True

# Construct from a list of tuples
pairs = [("alice", 95), ("bob", 80)]
scores = dict(pairs)

# Construct from two parallel lists with zip
names  = ["alice", "bob", "charlie"]
ages   = [30, 25, 35]
people = dict(zip(names, ages))

# Read a config file directly
import csv
with open("config.csv") as f:
    config = dict(csv.reader(f))
The dict(zip(...)) idiom is especially common when one part of your code produces lists and another consumes a dict. The connection between .items() and dict() is symmetric: anything you can iterate from one, you can construct into the other. For the deeper zip mechanics, see our enumerate and zip explainer.

Dict Comprehension: Iterate and Transform

A dict comprehension iterates a source and builds a new dict in one expression. Three patterns cover almost every real use:

Filter

scores = {"alice": 95, "bob": 80, "charlie": 50, "diana": 30}

passing = {name: score for name, score in scores.items() if score >= 60}
# {'alice': 95, 'bob': 80}

Transform values

doubled = {name: score * 2 for name, score in scores.items()}
# {'alice': 190, 'bob': 160, 'charlie': 100, 'diana': 60}

Swap keys and values

by_score = {score: name for name, score in scores.items()}
# {95: 'alice', 80: 'bob', 50: 'charlie', 30: 'diana'}
If the values aren't unique, the swap loses entries (last writer wins). For non-unique values, build a list per key with collections.defaultdict instead.

Group by, the defaultdict way

The most common reason a comprehension isn't enough: grouping. Suppose you have a list of users and you want to group them by department.
from collections import defaultdict

users = [
    {"name": "alice",   "dept": "eng"},
    {"name": "bob",     "dept": "sales"},
    {"name": "charlie", "dept": "eng"},
    {"name": "diana",   "dept": "sales"},
]

by_dept = defaultdict(list)
for u in users:
    by_dept[u["dept"]].append(u["name"])

print(dict(by_dept))
# {'eng': ['alice', 'charlie'], 'sales': ['bob', 'diana']}
defaultdict(list) auto-creates an empty list the first time you access a missing key, so you skip the "is this key already here?" check. For grouping, summing, or counting per key, it's the cleanest pattern Python offers. collections.Counter is the specialized version for counting only.

Aggregation Without an Explicit Loop

Once you see views as iterables, the next leap is realizing built-in aggregations (sum, max, min, any, all) take any iterable. You almost never need to write an explicit loop for these.
scores = {"alice": 95, "bob": 80, "charlie": 70, "diana": 30}

total       = sum(scores.values())              # 275
highest     = max(scores.values())              # 95
lowest_name = min(scores, key=scores.get)       # 'diana'
any_failing = any(v < 60 for v in scores.values())   # True
all_passing = all(v >= 60 for v in scores.values())  # False
Two patterns worth highlighting. First, max(scores) alone returns the alphabetically largest key; pass key=scores.get to compare by value instead. The same trick works for min. Second, generator expressions inside any and all short-circuit, so they stop scanning the moment the answer is decided. For a 10,000-entry dict where the first failing score lives near the start, any(v < 60 for v in scores.values()) can finish in microseconds.

Iterating Two Dicts in Parallel

For matched-up iteration across two dicts (same keys, complementary values), you have three options. Pick by intent.

Option 1: iterate keys, look up in both

names = {"alice": "Alice Smith", "bob": "Bob Jones"}
ages  = {"alice": 30, "bob": 25}

for key in names:
    print(names[key], ages[key])
Simple and explicit, but relies on both dicts having the same key set. A missing key raises KeyError.

Option 2: zip the views

for full_name, age in zip(names.values(), ages.values()):
    print(full_name, age)
This works because views are iterables in insertion order. It's compact but pairs by position, not by key, so it assumes both dicts have the same insertion order. For more on zip behavior, see our enumerate and zip explainer.

Option 3: ChainMap for fall-through lookup

from collections import ChainMap

defaults = {"theme": "light", "size": "medium"}
user     = {"theme": "dark"}

settings = ChainMap(user, defaults)
for key in settings:
    print(key, settings[key])
# theme dark
# size medium
ChainMap doesn't merge; it provides a layered lookup where the first dict wins. (Confusingly, this is the opposite of dict | dict; for the contrast, see our merge dicts explainer.) Use it when you want to iterate as if the dicts were merged without paying the merge cost.

Performance Comparison

For a dict with 10,000 entries, CPython 3.12 produces these rough timings per iteration (single-threaded):
PatternTime per full iterationNotes
for k in d~120 µsbaseline, fastest
for k in d.keys()~120 µsidentical to direct iter
for v in d.values()~120 µssame cost
for k, v in d.items()~180 µs~50% slower due to tuple unpacking
for k in list(d)~220 µsmaterialization cost
for k in sorted(d)~600 µsfull sort each call
Two takeaways. First, .items() isn't free: it pays a tuple-construction cost per pair. If you genuinely only need one half, use .keys() or .values() directly. Second, list(d) and sorted(d) both materialize; reach for them when you actually need the snapshot or the order, not by reflex.

Common Mistakes

Five traps to watch for:Mistake 1: treating views as lists. d.keys()[0] raises TypeError. If you need indexing, materialize: list(d.keys())[0] (or better, next(iter(d)) for the first key only).Mistake 2: mutating size during iteration. RuntimeError the moment you delete or add a key. Pick one of the three fixes above.Mistake 3: assuming order is sorted. Order is insertion, not lexicographic. If you need sorted, pass through sorted().Mistake 4: using .items() when only one half is needed. for k, v in d.items(): use(k) wastes a tuple per iteration. Just write for k in d.Mistake 5: forgetting .values() can have duplicates. sum(d.values()) handles duplicates fine. But set(d.values()) drops them, which is sometimes what you want and sometimes a bug. Pick deliberately.Mistake 6: relying on iteration order across dict reconstruction. Order is preserved through normal mutation and serialization, but operations that rebuild the dict (a comprehension, dict(d), **d unpacking into a new literal) follow the order of the source you iterated. If you sort, then comprehend, the result is sorted; if you iterate the original, it isn't. Two steps don't get conflated; each one preserves the order of its input.

Frequently Asked Questions

How do you iterate over a dictionary in Python?

Use one of four patterns. for key in my_dict iterates the keys (the default). for key in my_dict.keys() does the same, more explicitly. for value in my_dict.values() iterates the values. for key, value in my_dict.items() iterates pairs and unpacks them. Pick .items() whenever you need both; pick the explicit form for clarity in shared code.

Are dictionary keys ordered in Python?

Yes, since Python 3.7. Iteration follows the order in which keys were inserted. This was an implementation detail in CPython 3.6 and became a language guarantee in 3.7. From 3.8 onward, you can also call reversed(my_dict) to iterate in reverse-insertion order. For older Pythons, use collections.OrderedDict explicitly.

Can you modify a dictionary while iterating over it?

Not safely. Adding or removing keys during iteration raises RuntimeError: dictionary changed size during iteration. Three safe fixes: iterate over a list copy (list(d.items())), collect keys to remove first then delete after the loop, or use a dict comprehension to build a new dictionary. Mutating existing values during iteration is fine; only changing the key set is the trap.

What are dict_keys, dict_values, and dict_items in Python?

They are view objects: dynamic windows onto the dictionary's keys, values, or key-value pairs. They are not lists; they reflect changes to the source dictionary in real time. dict_keys and dict_items behave like sets and support set operations (intersection, union, difference). dict_values does not, because values are not required to be unique or hashable.

How do you iterate a dictionary sorted by value?

Use sorted() with a key function: for k, v in sorted(my_dict.items(), key=lambda kv: kv[1]). Pass reverse=True for descending order. To grab the top-N entries, combine with slicing: sorted(my_dict.items(), key=lambda kv: kv[1], reverse=True)[:5]. For frequency counts specifically, collections.Counter has a most_common() method that does the same job in one call.

The Bottom Line: Four Patterns, Three View Types, One Superpower

Iterating a Python dictionary is a four-pattern problem: direct, .keys(), .values(), or .items(). The objects returned by the latter three are view objects, not lists, which means they stay live with the source and they behave like sets where the values allow it. That set behavior unlocks set operations on .keys() and .items(): intersection for common keys, difference for missing or orphan keys, union and symmetric difference for the rarer cases. Insertion order has been guaranteed since Python 3.7, and reversed() works on dicts from 3.8 onward. The mutation-during-iteration trap has three reliable fixes; pick the one that matches your dict size and intent. With that toolkit, every dict-iteration question becomes a 30-second decision. For the rest of the most-asked Python concept questions, browse the full Python Concepts Explained index.

Drill Dict Patterns Until They're Reflex

CodeGym's Python track turns dict-iteration idioms into muscle memory through 800+ hands-on tasks across 62 levels. The AI validator checks every submission in seconds; the AI mentor explains what broke when you get stuck. First level free; full plan on the pricing page. Learn Python with hands-on practice →

Learn more about our mission and terms of service. Published article was last reviewed on 2026-05-29.