Skip to content

OrderedDict and ChainMap

These are specialized dict types for specific use cases.

Mental Model

OrderedDict is a dict that treats key order as first-class — you can move keys to either end or compare two dicts by order, not just content. ChainMap is a stack of dicts searched top-down: the first dict containing a key wins. Think of it as layered configuration (command-line args shadow environment vars, which shadow defaults) without merging anything.


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

```python 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()

```python 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

```python 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

```python 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

```python

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

```python 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:

```python 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:

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

Use Case: Config Layering

```python 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

```python 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

```python 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

```python

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)

Exercises

Exercise 1. Write a simplified LRUCache class using OrderedDict that supports get(key) and put(key, value) with a given capacity. When the cache exceeds capacity, evict the least recently used item. Demonstrate by inserting keys "a", "b", "c" into a cache of capacity 2, then verify that "a" was evicted.

Solution to Exercise 1

```python 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)
    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)

Test

cache = LRUCache(2) cache.put("a", 1) cache.put("b", 2) cache.put("c", 3) # Evicts "a" print(cache.get("a")) # -1 (evicted) print(cache.get("b")) # 2 print(cache.get("c")) # 3 ```


Exercise 2. Write a function merge_configs that takes any number of dictionaries (from lowest to highest priority) and returns a ChainMap. Then write a function get_config_value that takes the ChainMap and a key, returning the value or a default string "NOT_SET" if the key is not found.

Solution to Exercise 2

```python from collections import ChainMap

def merge_configs(dicts): # Reverse so last dict has highest priority return ChainMap(reversed(dicts))

def get_config_value(config, key): return config.get(key, "NOT_SET")

Test

defaults = {"host": "localhost", "port": 80, "debug": False} user = {"port": 8080} cli = {"debug": True}

config = merge_configs(defaults, user, cli) print(get_config_value(config, "debug")) # True (from cli) print(get_config_value(config, "port")) # 8080 (from user) print(get_config_value(config, "host")) # localhost (from defaults) print(get_config_value(config, "ssl")) # NOT_SET ```


Exercise 3. Write a function ordered_unique that takes a list of (key, value) pairs and returns an OrderedDict containing only the first occurrence of each key, preserving insertion order. For example, ordered_unique([("a", 1), ("b", 2), ("a", 3), ("c", 4)]) should return OrderedDict([("a", 1), ("b", 2), ("c", 4)]).

Solution to Exercise 3

```python from collections import OrderedDict

def ordered_unique(pairs): result = OrderedDict() for key, value in pairs: if key not in result: result[key] = value return result

Test

data = [("a", 1), ("b", 2), ("a", 3), ("c", 4)] result = ordered_unique(data) print(result)

OrderedDict([('a', 1), ('b', 2), ('c', 4)])

```