Skip to content

OrderedDict and ChainMap

These are specialized dict types for specific use cases.


OrderedDict

History

Before Python 3.7, regular dicts didn't preserve insertion order. OrderedDict was created to guarantee order.

Since Python 3.7: Regular dict maintains insertion order. Use OrderedDict only for its extra features.

Creating OrderedDict

from collections import OrderedDict

od = OrderedDict()
od['a'] = 1
od['b'] = 2
od['c'] = 3

# Or from list of tuples
od = OrderedDict([('a', 1), ('b', 2), ('c', 3)])

Special Features

move_to_end()

od = OrderedDict([('a', 1), ('b', 2), ('c', 3)])

od.move_to_end('a')             # Move to end
print(list(od.keys()))          # ['b', 'c', 'a']

od.move_to_end('c', last=False) # Move to beginning
print(list(od.keys()))          # ['c', 'b', 'a']

popitem() with last parameter

od = OrderedDict([('a', 1), ('b', 2), ('c', 3)])

od.popitem(last=True)   # Remove last: ('c', 3)
od.popitem(last=False)  # Remove first: ('a', 1)

Use Case: LRU Cache

from collections import OrderedDict

class LRUCache:
    def __init__(self, capacity):
        self.cache = OrderedDict()
        self.capacity = capacity

    def get(self, key):
        if key not in self.cache:
            return -1
        self.cache.move_to_end(key)  # Mark as recently used
        return self.cache[key]

    def put(self, key, value):
        if key in self.cache:
            self.cache.move_to_end(key)
        self.cache[key] = value
        if len(self.cache) > self.capacity:
            self.cache.popitem(last=False)  # Remove oldest

cache = LRUCache(2)
cache.put('a', 1)
cache.put('b', 2)
cache.get('a')      # Returns 1, moves 'a' to end
cache.put('c', 3)   # Evicts 'b' (oldest)

OrderedDict vs dict

Feature dict (3.7+) OrderedDict
Preserves order
move_to_end()
popitem(last=False)
Equality considers order
Memory Less More
# Order matters for OrderedDict equality
from collections import OrderedDict

d1 = {'a': 1, 'b': 2}
d2 = {'b': 2, 'a': 1}
print(d1 == d2)  # True (dict ignores order)

od1 = OrderedDict([('a', 1), ('b', 2)])
od2 = OrderedDict([('b', 2), ('a', 1)])
print(od1 == od2)  # False (order matters!)

ChainMap

A ChainMap groups multiple dicts into a single view without copying.

Creating ChainMap

from collections import ChainMap

defaults = {'color': 'red', 'size': 'medium'}
user_prefs = {'color': 'blue'}

combined = ChainMap(user_prefs, defaults)

Lookup Behavior

Searches dicts in order, returns first match:

combined = ChainMap(user_prefs, defaults)

print(combined['color'])  # 'blue' (from user_prefs)
print(combined['size'])   # 'medium' (from defaults)

Modifications

Writes only affect the first dict:

combined['new_key'] = 'value'
print(user_prefs)   # {'color': 'blue', 'new_key': 'value'}
print(defaults)     # {'color': 'red', 'size': 'medium'} (unchanged)

Use Case: Config Layering

from collections import ChainMap

# Priority: CLI > Environment > Config File > Defaults
cli_args = {'debug': True}
env_vars = {'port': 8080, 'host': '0.0.0.0'}
config_file = {'port': 3000, 'timeout': 30}
defaults = {'debug': False, 'port': 80, 'host': 'localhost', 'timeout': 60}

config = ChainMap(cli_args, env_vars, config_file, defaults)

print(config['debug'])    # True (from cli_args)
print(config['port'])     # 8080 (from env_vars)
print(config['timeout'])  # 30 (from config_file)
print(config['host'])     # '0.0.0.0' (from env_vars)

Use Case: Scoped Variables

from collections import ChainMap

# Simulate variable scoping (like Python's LEGB)
global_vars = {'x': 1, 'y': 2}
local_vars = {'x': 10}  # Shadows global x

scope = ChainMap(local_vars, global_vars)
print(scope['x'])  # 10 (local shadows global)
print(scope['y'])  # 2 (from global)

ChainMap Methods

combined = ChainMap(user_prefs, defaults)

# Access underlying dicts
combined.maps        # [user_prefs, defaults]

# Create new child scope
child = combined.new_child({'temp': 'value'})
print(child.maps)    # [{'temp': 'value'}, user_prefs, defaults]

# Get parent scope
parent = child.parents
print(parent.maps)   # [user_prefs, defaults]

ChainMap vs dict.update()

Aspect ChainMap dict.update()
Copies data ❌ No (view) ✅ Yes
Reflects changes ✅ Yes ❌ No
Memory Lower Higher
Layered access ✅ Yes ❌ Flattened
# ChainMap reflects changes
d1 = {'a': 1}
d2 = {'b': 2}
cm = ChainMap(d1, d2)

d1['a'] = 100
print(cm['a'])  # 100 (reflects change!)

# dict.update() doesn't
merged = {}
merged.update(d2)
merged.update(d1)

d1['a'] = 200
print(merged['a'])  # 100 (snapshot, not live)

Summary

Type Use When
OrderedDict Need move_to_end(), order-aware equality, or LRU cache
ChainMap Layered config, scoped lookups, non-copying dict merge
Regular dict Most other cases (ordered since 3.7)