Iterating Over Dictionaries Using 'for' Loops in Python

Dictionaries sit at the core of how Python works internally. Modules, classes, object attributes, and global variables are all stored in dictionaries behind the scenes. Learning how to iterate over them with for loops is one of the foundational skills that separates someone who writes Python from someone who writes Pythonic Python. This article walks through every technique for looping over dictionary keys, values, and key-value pairs, with verified code examples you can run right now.

The Python official documentation defines a dictionary as "an associative array, where arbitrary keys are mapped to values" (Source: Python Glossary). It further specifies that the keys can be any object that implements the __hash__() and __eq__() methods. But once you have data stored in a dictionary, you need to know how to get it back out, and the for loop is the primary tool for that job.

How Python Dictionaries Became Iterable

Dictionaries did not always support direct iteration. In Python 2.2, PEP 234 introduced the iterator protocol to dict objects, which allowed programmers to iterate over keys without building a separate list first. The iter(d) call provided direct access to an iterator object for the dictionary's keys, eliminating the need to call d.keys() and create a full list in memory just to loop through the entries.

The bigger transformation came with Python 3. PEP 3106, authored by Guido van Rossum, overhauled the dictionary API. The separate list-based methods (.keys(), .values(), .items()) and their iterator-based counterparts (.iterkeys(), .itervalues(), .iteritems()) were replaced with a single set of methods that return lightweight view objects. According to the Python documentation, these view objects "provide a dynamic view on the dictionary's entries, which means that when the dictionary changes, the view reflects these changes" (Source: Python Glossary — dictionary view).

Another critical development was the guarantee of insertion order. In CPython 3.6, Raymond Hettinger's compact dictionary design was implemented, replacing the dictionary's internal data structure with a dense entries array and a separate sparse index table. This layout happened to preserve insertion order as a side effect. Then, in December 2017, Guido van Rossum settled the matter on the python-dev mailing list with a three-word decree: "Make it so." His full message read, "'Dict keeps insertion order' is the ruling." (Source: python-dev mailing list, December 15, 2017). This was formalized in the Python 3.7 release notes, which state that "the insertion-order preservation nature of dict objects has been declared to be an official part of the Python language spec" (Source: What's New in Python 3.7).

This history matters for iteration because it means that from Python 3.7 onward, every for loop over a dictionary will process entries in the exact order they were inserted. You can rely on this behavior across all conforming Python implementations.

Iterating Over Keys with a Basic for Loop

The simplest way to iterate over a dictionary is to place it directly after the in keyword in a for statement. When you do this, Python calls the dictionary's __iter__() method, which returns an iterator over the dictionary's keys. This is the same as calling iter(d) explicitly.

server_config = {
    "host": "192.168.1.100",
    "port": 8443,
    "protocol": "https",
    "timeout": 30
}

for key in server_config:
    print(f"{key} = {server_config[key]}")

Output:

host = 192.168.1.100
port = 8443
protocol = https
timeout = 30

Inside the loop body, the variable key receives the name of each key on every iteration. To access the corresponding value, you use bracket notation: server_config[key]. This approach works, but it requires a separate dictionary lookup on each pass through the loop. That means Python has to compute the hash of the key and locate it in the hash table for every iteration, even though the loop already has access to the internal entry.

You can also call .keys() explicitly, which returns a dict_keys view object:

for key in server_config.keys():
    print(key)

The result is identical. As the Python Anti-Patterns documentation notes, citing PEP 20, "there should be one -- and preferably only one -- obvious way to do it." In practice, writing for key in d and for key in d.keys() produce the same behavior (Source: QuantifiedCode Python Anti-Patterns). The explicit .keys() call is useful when you want to make your intent unmistakably clear to other readers of your code, or when you need to perform set operations on the keys view (such as intersection or difference).

Note

The dict_keys view object returned by .keys() is not a list. It is a dynamic view that reflects changes to the dictionary in real time. If you need a static snapshot of the keys (for example, to modify the dictionary during iteration), you must convert it to a list explicitly with list(d.keys()).

Iterating Over Values with .values()

When you only need the values and not the keys, the .values() method returns a dict_values view object that you can loop over directly:

cpu_temps = {
    "core_0": 62.5,
    "core_1": 65.3,
    "core_2": 61.8,
    "core_3": 67.1
}

total = 0
count = 0

for temp in cpu_temps.values():
    total += temp
    count += 1

average = total / count
print(f"Average CPU temperature: {average:.1f}\u00b0C")

Output:

Average CPU temperature: 64.2°C

In this example, the keys (core names) are irrelevant to the calculation, so .values() is the right choice. The loop variable temp receives each floating-point value in insertion order. For the specific case of summing values, Python provides a more concise approach using the built-in sum() function, which accepts any iterable:

average = sum(cpu_temps.values()) / len(cpu_temps)
print(f"Average CPU temperature: {average:.1f}\u00b0C")

Output:

Average CPU temperature: 64.2°C

The sum() function internally iterates over the values view and accumulates the total. This is both faster and easier to read, but the explicit for loop version gives you more flexibility when the logic is more complex than a simple sum.

Iterating Over Key-Value Pairs with .items()

The .items() method is the workhorse of dictionary iteration. It returns a dict_items view object that yields each entry as a two-element tuple of (key, value). Combined with tuple unpacking in the for statement, it lets you access both the key and value in a single, readable line.

firewall_rules = {
    "ssh": "allow from 10.0.0.0/8",
    "http": "allow from any",
    "https": "allow from any",
    "telnet": "deny from any",
    "ftp": "deny from any"
}

for service, rule in firewall_rules.items():
    print(f"{service:10s} -> {rule}")

Output:

ssh        -> allow from 10.0.0.0/8
http       -> allow from any
https      -> allow from any
telnet     -> deny from any
ftp        -> deny from any

The Python Anti-Patterns documentation describes the .items() approach as the preferred way to iterate when you need both keys and values — using two loop variables with dictionary.items() rather than iterating over keys and performing a d[key] lookup inside the loop body, which is functional but redundant since the iterator already has access to the value internally (Source: QuantifiedCode Python Anti-Patterns).

In Python 2, .items() returned a list of tuples, which meant the entire dictionary contents were copied into a new list in memory. Python 3 changed this so that .items() returns a view object instead, which consumes almost no additional memory regardless of the dictionary's size. As PEP 469 explains, the Python 2 .iteritems() method was the memory-efficient alternative, and its behavior is essentially what .items() provides in Python 3 (Source: PEP 469).

Tuple Unpacking and Why It Matters

The for key, value in d.items() syntax relies on a Python feature called tuple unpacking (also known as destructuring or sequence unpacking). When the .items() view yields a tuple like ("ssh", "allow from 10.0.0.0/8"), Python automatically assigns the first element to the first variable and the second element to the second variable.

To see this mechanism more clearly, consider what happens if you use a single loop variable instead:

user_roles = {
    "alice": "admin",
    "bob": "editor",
    "carol": "viewer"
}

for item in user_roles.items():
    print(type(item), item)

Output:

<class 'tuple'> ('alice', 'admin')
<class 'tuple'> ('bob', 'editor')
<class 'tuple'> ('carol', 'viewer')

Each item is a plain tuple. When you write for key, value in d.items(), Python performs the equivalent of key, value = item at the start of each iteration. This is cleaner than writing item[0] and item[1], and it makes your intent self-documenting through descriptive variable names.

You can even use nested unpacking if your dictionary values are themselves sequences:

employee_data = {
    "E001": ("Alice", "Engineering", 95000),
    "E002": ("Bob", "Marketing", 82000),
    "E003": ("Carol", "Engineering", 91000)
}

for emp_id, (name, dept, salary) in employee_data.items():
    print(f"{emp_id}: {name} in {dept}, salary ${salary:,}")

Output:

E001: Alice in Engineering, salary $95,000
E002: Bob in Marketing, salary $82,000
E003: Carol in Engineering, salary $91,000

Here, the value side of each entry is itself a three-element tuple, and Python unpacks it into three separate variables inside the parentheses. This nested unpacking is a powerful pattern for working with structured data stored in dictionaries.

Pro Tip

Give your loop variables meaningful names that reflect the data they hold. Instead of for k, v in d.items(), write for hostname, ip_address in dns_records.items(). This makes your code self-documenting and dramatically easier to debug.

Iterating Over Nested Dictionaries

Real-world data is rarely flat. Configuration files, API responses, and database records often contain dictionaries nested inside other dictionaries. Iterating over these structures requires combining loops with type checking to handle each level of depth.

network_devices = {
    "router_01": {
        "ip": "10.0.0.1",
        "status": "active",
        "interfaces": {"eth0": "up", "eth1": "down"}
    },
    "switch_02": {
        "ip": "10.0.0.2",
        "status": "active",
        "interfaces": {"eth0": "up", "eth1": "up"}
    }
}

for device_name, device_info in network_devices.items():
    print(f"\n[{device_name}]")
    for attr, value in device_info.items():
        if isinstance(value, dict):
            print(f"  {attr}:")
            for sub_key, sub_value in value.items():
                print(f"    {sub_key}: {sub_value}")
        else:
            print(f"  {attr}: {value}")

Output:

[router_01]
  ip: 10.0.0.1
  status: active
  interfaces:
    eth0: up
    eth1: down

[switch_02]
  ip: 10.0.0.2
  status: active
  interfaces:
    eth0: up
    eth1: up

The outer loop iterates over the top-level device names and their associated configuration dictionaries. The inner loop processes each attribute within a device. When it encounters a value that is itself a dictionary (detected with isinstance(value, dict)), it drops into a third level of iteration.

For deeply nested structures with unpredictable depth, a recursive function provides a more scalable solution:

def print_nested(d, indent=0):
    for key, value in d.items():
        prefix = "  " * indent
        if isinstance(value, dict):
            print(f"{prefix}{key}:")
            print_nested(value, indent + 1)
        else:
            print(f"{prefix}{key}: {value}")

print_nested(network_devices)

This recursive approach handles any number of nesting levels without requiring you to write additional loops manually.

Output:

router_01:
  ip: 10.0.0.1
  status: active
  interfaces:
    eth0: up
    eth1: down
switch_02:
  ip: 10.0.0.2
  status: active
  interfaces:
    eth0: up
    eth1: up

Dictionary Comprehensions as Iterative Tools

Dictionary comprehensions provide a concise syntax for creating new dictionaries by iterating over existing ones. They follow the same iteration mechanics as for loops but express the operation in a single expression.

raw_scores = {
    "math": 78,
    "physics": 92,
    "chemistry": 85,
    "biology": 64,
    "english": 88
}

# Create a new dictionary containing only passing scores (>= 70)
passing = {subject: score for subject, score in raw_scores.items() if score >= 70}
print(passing)

Output:

{'math': 78, 'physics': 92, 'chemistry': 85, 'english': 88}

The comprehension iterates over each key-value pair, applies the filter condition (if score >= 70), and includes only matching pairs in the new dictionary. This is equivalent to the following explicit loop:

passing = {}
for subject, score in raw_scores.items():
    if score >= 70:
        passing[subject] = score

Both produce the same result, but the comprehension form is more compact. You can also use comprehensions to transform keys or values during iteration:

# Swap keys and values
score_lookup = {score: subject for subject, score in raw_scores.items()}
print(score_lookup)

Output:

{78: 'math', 92: 'physics', 85: 'chemistry', 64: 'biology', 88: 'english'}
Warning

When swapping keys and values, be aware that dictionary keys must be unique. If two keys in the original dictionary share the same value, only the last one encountered will appear in the swapped dictionary. The earlier entry will be silently overwritten.

Mutating a Dictionary During Iteration

One of the most common pitfalls in dictionary iteration is attempting to add or remove keys while a for loop is actively iterating over the dictionary. Python explicitly raises a RuntimeError if the dictionary's size changes during iteration.

sessions = {
    "sess_001": {"user": "alice", "expired": True},
    "sess_002": {"user": "bob", "expired": False},
    "sess_003": {"user": "carol", "expired": True}
}

# This will raise RuntimeError
try:
    for session_id, data in sessions.items():
        if data["expired"]:
            del sessions[session_id]
except RuntimeError as e:
    print(f"Error: {e}")

Output:

Error: dictionary changed size during iteration

The CPython C API documentation is explicit on this point: a dictionary should not be mutated during iteration, though modifying values of existing keys is permitted as long as the set of keys does not change (Source: Python C API — Dictionary Objects).

Raymond Hettinger put it more colorfully in his 2013 PyCon talk, "Transforming Code into Beautiful, Idiomatic Python," saying that if you mutate something while you are iterating over it, you are "living in a state of sin and deserve whatever happens to you" (Source: Notes from Raymond Hettinger's PyCon US 2013 talk).

The safe approach is to iterate over a snapshot of the keys (a list copy) and then modify the original dictionary:

# Safe: iterate over a copy of the keys
for session_id in list(sessions.keys()):
    if sessions[session_id]["expired"]:
        del sessions[session_id]

print(sessions)

Output:

{'sess_002': {'user': 'bob', 'expired': False}}

By calling list(sessions.keys()), you create a static list of keys before the loop begins. The loop then iterates over this list, not the dictionary itself, so deletions from the dictionary do not interfere with the iteration. An alternative approach is to build a new dictionary using a comprehension that excludes the unwanted entries:

sessions = {
    "sess_001": {"user": "alice", "expired": True},
    "sess_002": {"user": "bob", "expired": False},
    "sess_003": {"user": "carol", "expired": True}
}

active_sessions = {
    sid: data for sid, data in sessions.items() if not data["expired"]
}
print(active_sessions)

This comprehension-based approach is often preferred because it avoids mutation entirely and produces a clean, new dictionary.

Note that while adding or removing keys during iteration is forbidden, modifying the values of existing keys is safe:

prices = {"apple": 1.20, "banana": 0.50, "cherry": 3.00}

# Safe: modifying values without changing keys
for fruit in prices:
    prices[fruit] = round(prices[fruit] * 1.10, 2)

for fruit, price in prices.items():
    print(f"{fruit}: {price:.2f}")

Output:

apple: 1.32
banana: 0.55
cherry: 3.30

Note: using print(prices) directly would show 3.3 for cherry, because round(3.00 * 1.10, 2) returns the float 3.3 — Python drops the trailing zero in its default float representation. The :.2f format specifier enforces two decimal places when you need consistent display.

Using enumerate() and zip() with Dictionaries

The built-in enumerate() function pairs each element of an iterable with its positional index. When applied to a dictionary (or a dictionary view), it gives you a counter alongside each key, value, or key-value pair:

priorities = {
    "patch_critical_vuln": "urgent",
    "update_firewall_rules": "high",
    "review_access_logs": "medium",
    "document_procedures": "low"
}

for index, (task, priority) in enumerate(priorities.items(), start=1):
    print(f"{index}. [{priority.upper()}] {task}")

Output:

1. [URGENT] patch_critical_vuln
2. [HIGH] update_firewall_rules
3. [MEDIUM] review_access_logs
4. [LOW] document_procedures

The enumerate() function wraps each (key, value) tuple from .items() inside another tuple that includes the index. By using nested unpacking in the for statement (index, (task, priority)), you can access all three values cleanly.

The zip() function is useful when you want to iterate over two or more dictionaries in parallel. It pairs up entries from each iterable by position:

q1_revenue = {"product_a": 50000, "product_b": 32000, "product_c": 78000}
q2_revenue = {"product_a": 55000, "product_b": 29000, "product_c": 81000}

for (product, q1), (_, q2) in zip(q1_revenue.items(), q2_revenue.items()):
    change = q2 - q1
    direction = "up" if change > 0 else "down" if change < 0 else "flat"
    print(f"{product}: ${change:+,} ({direction})")

Output:

product_a: $+5,000 (up)
product_b: $-3,000 (down)
product_c: $+3,000 (up)

This pattern works reliably because dictionaries preserve insertion order in Python 3.7 and later, so the entries from both dictionaries are guaranteed to be aligned by position as long as they were populated in the same order. If the dictionaries might have different keys or different orderings, you should iterate over one and look up the corresponding values in the other using bracket notation.

Performance Considerations

Understanding the performance characteristics of different iteration approaches helps you choose the right one for your specific context. Here is a summary of the trade-offs.

Iterating directly with for key in d is the fastest option when you only need keys. It avoids creating any intermediate objects and walks the dictionary's internal dense entries array directly. If you also need the corresponding value, accessing it via d[key] inside the loop body adds a hash table lookup on every iteration.

Using for key, value in d.items() avoids the redundant lookup because both the key and value are yielded together from the internal entries array. For large dictionaries where you need both keys and values, this is measurably faster than the d[key] approach.

Raymond Hettinger's compact dictionary design, implemented in CPython 3.6, made iteration faster overall. Because the key-value entries are stored in a dense, contiguous array (rather than scattered across a sparse hash table), iterating over them benefits from better CPU cache locality. In his original proposal, Hettinger described the previous layout as "unnecessarily inefficient" because it stored 24-byte entries in a sparse table, whereas the new layout places them in a dense table referenced by a sparse index (Source: python-dev mailing list, December 10, 2012).

The view objects returned by .keys(), .values(), and .items() are lightweight wrappers that consume a fixed amount of memory regardless of the dictionary's size. They do not copy the dictionary's contents. This is in contrast to Python 2's .items(), which created a full list copy.

Key Takeaways

  1. Default iteration yields keys: Writing for key in d iterates over the dictionary's keys. This is equivalent to for key in d.keys(), but without the explicit method call.
  2. Use .items() for key-value access: When you need both the key and the value, for key, value in d.items() is the idiomatic and most efficient approach. Avoid iterating over keys and then performing d[key] lookups inside the loop body.
  3. Use .values() when keys are irrelevant: If your computation only involves the values (such as summing, averaging, or filtering), .values() communicates your intent clearly and avoids unused variables.
  4. Insertion order is guaranteed: Since Python 3.7, dictionaries preserve and iterate in insertion order. This is a language-level guarantee, not just an implementation detail.
  5. Never add or remove keys during iteration: Changing the size of a dictionary while a loop is iterating over it raises a RuntimeError. Use list(d.keys()) to create a snapshot, or build a new dictionary with a comprehension.
  6. Modifying values during iteration is safe: You can change the value associated with an existing key inside a loop without triggering an error, as long as you do not add or remove keys.
  7. View objects are lightweight: The objects returned by .keys(), .values(), and .items() are dynamic views, not copies. They reflect changes to the underlying dictionary and use negligible memory.
  8. Comprehensions are concise iteration: Dictionary comprehensions combine iteration, filtering, and transformation into a single expression. They are the Pythonic way to derive new dictionaries from existing ones.

Iterating over dictionaries with for loops is straightforward once you understand the three core methods (.keys(), .values(), and .items()) and the rules around mutation. Choose the method that matches the data you actually need in the loop body, give your variables descriptive names, and be deliberate about whether you are modifying the dictionary or creating a new one. These habits will make your Python code both more readable and more performant.

back to articles