Python Dictionaries — Language Specification
1. Spec Reference
- Primary source: Python Language Reference, §3.2 — Mapping types https://docs.python.org/3/reference/datamodel.html#mapping-types
- Built-in dict type: https://docs.python.org/3/library/stdtypes.html#mapping-types-dict
- Dict display grammar: §6.2.6 https://docs.python.org/3/reference/expressions.html#dictionary-displays
collections.OrderedDict, defaultdict, ChainMap, Counter: https://docs.python.org/3/library/collections.html typing.TypedDict: https://docs.python.org/3/library/typing.html#typing.TypedDict - Python 3.12 standard: All rules here apply to CPython 3.12 unless otherwise noted.
2.1 Dictionary Display (Literal)
dict_display ::= "{" [key_datum_list | dict_comprehension] "}"
key_datum_list ::= key_datum ("," key_datum)* [","]
key_datum ::= expression ":" expression
| "**" or_expr
dict_comprehension::= expression ":" expression comp_for
2.2 Subscription (Key Lookup and Assignment)
subscription ::= primary "[" expression_list "]"
# For dict: key lookup → d[key]
# For dict: key assignment → d[key] = value
# For dict: key deletion → del d[key]
2.3 Dict Merge Operators (Python 3.9+, PEP 584)
merge_expr ::= dict_expr "|" dict_expr # returns new dict
update_expr ::= dict_expr "|=" dict_expr # in-place update (augmented assignment)
3. Core Rules and Constraints
3.1 Dictionary Characteristics
- Ordered: Since Python 3.7, dicts preserve insertion order (was implementation detail in 3.6, guaranteed in 3.7).
- Mutable: key-value pairs can be added, updated, or deleted.
- Keys must be hashable: keys must define
__hash__ and __eq__. - Values can be any object: no restriction on value types.
- Keys are unique: duplicate key assignment overwrites the previous value.
- No duplicate keys in a literal:
{"a": 1, "a": 2} is valid syntax; the second value wins.
3.2 Key Lookup
d[key] raises KeyError if key is not present. d.get(key) returns None if not present (no exception). d.get(key, default) returns default if not present. - Lookup uses
hash(key) then key.__eq__ for collision resolution.
3.3 Insertion Order (Python 3.7+)
dict is ordered by insertion order. - When iterating with
for k in d, d.keys(), d.values(), d.items() — order is insertion order. - Updating an existing key does not change its position; deletion and re-insertion moves it to the end.
3.4 Key Equality and Hashability
- Two keys are considered equal if
hash(k1) == hash(k2) and k1 == k2. 1 and True are equal (True == 1, hash(True) == hash(1) == 1); they are the same key. 1.0 and 1 are equal (1.0 == 1, hash(1.0) == hash(1)); same key. list, set, dict cannot be keys (unhashable). tuple and frozenset can be keys (if all elements are hashable).
3.5 Dict Merging (Python 3.9+)
d1 | d2 creates a new dict; for duplicate keys, d2 values win. d1 |= d2 updates d1 in-place; for duplicate keys, d2 values win. - Before Python 3.9: use
{**d1, **d2} or d1.update(d2).
3.6 Iteration During Mutation
- Adding or removing keys during iteration over a dict raises
RuntimeError in CPython 3.3+. - Modifying the value of an existing key during iteration is safe.
4. Type Rules (Dunder Methods and Protocols)
4.1 Mapping Protocol
object.__getitem__(self, key) -> value
# Called by d[key]. Raises KeyError if key absent.
object.__setitem__(self, key, value)
# Called by d[key] = value.
object.__delitem__(self, key)
# Called by del d[key]. Raises KeyError if absent.
object.__iter__(self) -> iterator
# Iterates over keys.
object.__len__(self) -> int
# Number of key-value pairs.
object.__contains__(self, key) -> bool
# Called by 'key in d'. Default falls back to __iter__.
4.2 collections.abc.Mapping Abstract Base
# Inheriting from Mapping requires only: __getitem__, __iter__, __len__
# Provides default implementations of: get, __contains__, keys, items, values, __eq__, __ne__
# Inheriting from MutableMapping additionally requires: __setitem__, __delitem__
# Provides: pop, popitem, clear, update, setdefault
4.3 Merge Protocol Dunders
object.__or__(self, other) -> dict # d1 | d2 (Python 3.9+)
object.__ior__(self, other) -> self # d1 |= d2
object.__ror__(self, other) -> dict # other | d1 (reflected)
4.4 __missing__ Protocol
object.__missing__(self, key) -> value
# Called by dict.__getitem__ if key is not found.
# Used by collections.defaultdict.
5. Behavioral Specification
5.1 dict() Constructor
dict() → empty dict. dict(**kwargs) → dict from keyword arguments. dict(mapping) → dict from any mapping. dict(iterable) → dict from iterable of (key, value) pairs. dict(mapping, **kwargs) → combines mapping and keyword args; kwargs override.
5.2 dict Methods (Full Spec)
| Method | Description | Returns |
d[key] | Get value; KeyError if absent | value |
d[key] = val | Set/update value | — |
del d[key] | Delete key; KeyError if absent | — |
key in d | Membership test (O(1)) | bool |
key not in d | Non-membership test | bool |
d.get(key[, default]) | Get value or default (no exception) | value or default |
d.setdefault(key[, default]) | Get value; insert default if missing | value |
d.pop(key[, default]) | Remove and return; KeyError if no default | value |
d.popitem() | Remove and return last inserted (LIFO); KeyError if empty | (key, value) |
d.update([other[, **kwargs]]) | Update from mapping/iterable/kwargs | None |
d.clear() | Remove all items | None |
d.copy() | Shallow copy | dict |
d.keys() | View of keys | dict_keys |
d.values() | View of values | dict_values |
d.items() | View of (key, value) pairs | dict_items |
5.3 View Objects (dict_keys, dict_values, dict_items)
- Views are dynamic: they reflect changes to the dict.
dict_keys and dict_items support set operations (|, &, -, ^) if values are hashable. dict_keys supports in test in O(1). - Views can be iterated but not indexed.
- Mutating the dict during iteration of a view raises
RuntimeError.
5.4 setdefault vs get vs d[key]
# d.get(key, default): read-only; does not modify dict
val = d.get("x", 0)
# d.setdefault(key, default): inserts if missing; modifies dict
val = d.setdefault("x", 0)
# d[key]: raises KeyError if missing
val = d["x"]
5.5 defaultdict (collections.defaultdict)
- Subclass of
dict; defines __missing__ to call the default_factory. defaultdict(list) — missing key returns [] and inserts it. defaultdict(int) — missing key returns 0. default_factory is called with no arguments.
5.6 ** Unpacking in Dict Literals
{**d1, **d2, "extra": val} creates a new dict. - For duplicate keys, the last occurrence wins.
- Available since Python 3.5 (PEP 448).
6. Defined vs Undefined Behavior
6.1 Defined
d[key] raises KeyError for absent keys. del d[key] raises KeyError for absent keys. - Insertion order preserved since Python 3.7 (spec guarantee).
d.popitem() removes the last inserted item (LIFO since Python 3.7). True and 1 are the same key; False and 0 are the same key. - Merging with
| or update: last-writer-wins for duplicate keys. - Views reflect the current state of the dict dynamically.
6.2 Undefined / Implementation-Defined
- Exact
id() of dict internals: implementation-defined. - Hash collision strategy: CPython uses open addressing with pseudo-random probing. The exact probing sequence is not specified.
- Dict resizing threshold: CPython resizes when load factor exceeds 2/3. The growth factor and exact table sizes are implementation details.
d.keys() vs d.values() consistency under concurrent modification: not defined for multithreaded use without the GIL.
7. Edge Cases from the Spec (CPython-Specific Notes)
7.1 True/1/1.0 Are the Same Key
d = {}
d[1] = "one"
d[True] = "true" # overwrites d[1]!
d[1.0] = "float one" # overwrites d[True]!
print(d) # {1: 'float one'}
print(len(d)) # 1
print(d[True]) # 'float one'
print(d[1.0]) # 'float one'
7.2 Dict Comprehension Duplicate Keys
# Last value wins for duplicate keys in comprehension:
d = {k % 3: k for k in range(9)}
print(d) # {0: 6, 1: 7, 2: 8} — last assignment wins
7.3 RuntimeError on Mutation During Iteration
d = {"a": 1, "b": 2, "c": 3}
try:
for k in d:
del d[k] # RuntimeError!
except RuntimeError as e:
print(e) # dictionary changed size during iteration
# Safe: iterate over a copy
for k in list(d.keys()):
del d[k]
print(d) # {}
7.4 setdefault for Grouping
data = [("Alice", 90), ("Bob", 85), ("Alice", 95), ("Bob", 88)]
groups = {}
for name, score in data:
groups.setdefault(name, []).append(score)
print(groups)
# {'Alice': [90, 95], 'Bob': [85, 88]}
# Equivalent with defaultdict:
from collections import defaultdict
groups2 = defaultdict(list)
for name, score in data:
groups2[name].append(score)
print(dict(groups2))
7.5 popitem() LIFO Order (Python 3.7+)
d = {"a": 1, "b": 2, "c": 3}
print(d.popitem()) # ('c', 3) — last inserted
print(d.popitem()) # ('b', 2)
print(d.popitem()) # ('a', 1)
7.6 Dict View Set Operations
d1 = {"a": 1, "b": 2, "c": 3}
d2 = {"b": 20, "c": 30, "d": 40}
common_keys = d1.keys() & d2.keys()
print(common_keys) # {'b', 'c'}
only_d1 = d1.keys() - d2.keys()
print(only_d1) # {'a'}
# items() supports set ops only if values are hashable:
common_items = d1.items() & d2.items()
print(common_items) # set() — no item is identical in both
7.7 | Merge Operator (Python 3.9+)
defaults = {"color": "blue", "size": "medium", "weight": 1.0}
overrides = {"color": "red", "material": "steel"}
config = defaults | overrides
print(config)
# {'color': 'red', 'size': 'medium', 'weight': 1.0, 'material': 'steel'}
# In-place:
defaults |= overrides
print(defaults)
# Same result; defaults modified in-place
8. Version History (PEPs and Python Versions)
| Feature | PEP | Version |
dict built-in | — | Python 1.0 |
dict.keys(), values(), items() returning views (not lists) | PEP 3106 | Python 3.0 |
Dict comprehensions {k: v for ...} | PEP 274 | Python 2.7 / 3.0 |
** unpacking in dict literals and calls | PEP 448 | Python 3.5 |
typing.TypedDict | PEP 589 | Python 3.8 |
| Insertion-order preservation as language spec guarantee | — | Python 3.7 |
| CPython 3.6 ordered dict (implementation detail) | — | Python 3.6 |
dict | dict merge operator | PEP 584 | Python 3.9 |
dict |= dict in-place merge | PEP 584 | Python 3.9 |
dict[K, V] subscript in type hints | PEP 585 | Python 3.9 |
typing.TypedDict with Required/NotRequired | PEP 655 | Python 3.11 |
9. Implementation-Specific Behavior
9.1 CPython dict Compact Representation
- Since CPython 3.6,
dict uses a compact representation: a sparse index array + a dense array of entries. - This reduces memory usage compared to the pre-3.6 open-addressing table.
- Insertion order is maintained by the dense entries array.
9.2 CPython dict Memory
- Empty dict:
sys.getsizeof({}) = 64 bytes (CPython 3.12 on 64-bit). - Each entry: ~50 bytes (key pointer + value pointer + hash).
9.3 CPython Resizing
dict resizes (doubles) when more than 2/3 of the index array is used. - Resize triggers rehashing of all entries.
- Deletion does not immediately shrink the dict; only resize does.
9.4 PyPy
- PyPy has a similar ordered dict implementation.
- May differ in memory usage and exact resize thresholds.
sys.getsizeof() results differ from CPython.
9.5 collections.OrderedDict vs dict
- In Python 3.7+,
dict preserves insertion order, making OrderedDict redundant for most uses. OrderedDict still has move_to_end(key, last=True) and supports comparison that considers order. - Two
OrderedDicts with same items in different order are NOT equal; two dicts are.
10. Spec Compliance Checklist
11. Official Examples (Runnable Python 3.10+)
from collections import defaultdict, Counter, ChainMap, OrderedDict
from typing import TypedDict
# ----------------------------------------------------------------
# 1. Creating dicts
# ----------------------------------------------------------------
d1 = {"name": "Alice", "age": 30}
d2 = dict(name="Bob", age=25)
d3 = dict([("x", 1), ("y", 2)])
d4 = {k: k**2 for k in range(5)}
print(d1) # {'name': 'Alice', 'age': 30}
print(d4) # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
# ----------------------------------------------------------------
# 2. Access patterns
# ----------------------------------------------------------------
d = {"a": 1, "b": 2}
print(d["a"]) # 1
print(d.get("c")) # None
print(d.get("c", 0)) # 0
print(d.setdefault("c", 0)) # inserts "c": 0, returns 0
print(d) # {'a': 1, 'b': 2, 'c': 0}
# ----------------------------------------------------------------
# 3. Mutation
# ----------------------------------------------------------------
d = {"a": 1}
d["b"] = 2 # add
d["a"] = 10 # update
del d["b"] # delete
print(d) # {'a': 10}
d.update({"b": 20, "c": 30})
print(d) # {'a': 10, 'b': 20, 'c': 30}
val = d.pop("b")
print(val, d) # 20 {'a': 10, 'c': 30}
last = d.popitem() # LIFO: ('c', 30)
print(last, d) # ('c', 30) {'a': 10}
# ----------------------------------------------------------------
# 4. dict views
# ----------------------------------------------------------------
d = {"x": 1, "y": 2, "z": 3}
keys = d.keys()
values = d.values()
items = d.items()
d["w"] = 4 # views update dynamically
print(list(keys)) # ['x', 'y', 'z', 'w']
print(list(values)) # [1, 2, 3, 4]
for k, v in items:
print(f"{k}={v}", end=" ") # x=1 y=2 z=3 w=4
print()
# ----------------------------------------------------------------
# 5. Key membership
# ----------------------------------------------------------------
d = {"a": 1, "b": 2}
print("a" in d) # True (O(1))
print("c" not in d) # True
print("a" in d.keys()) # True (equivalent)
print(1 in d.values()) # True (O(n))
# ----------------------------------------------------------------
# 6. ** unpacking (PEP 448)
# ----------------------------------------------------------------
defaults = {"debug": False, "timeout": 30}
overrides = {"timeout": 60, "verbose": True}
config = {**defaults, **overrides}
print(config) # {'debug': False, 'timeout': 60, 'verbose': True}
# ----------------------------------------------------------------
# 7. | merge operator (Python 3.9+, PEP 584)
# ----------------------------------------------------------------
base = {"a": 1, "b": 2}
patch = {"b": 20, "c": 30}
merged = base | patch
print(merged) # {'a': 1, 'b': 20, 'c': 30}
base |= patch # in-place
print(base) # {'a': 1, 'b': 20, 'c': 30}
# ----------------------------------------------------------------
# 8. defaultdict
# ----------------------------------------------------------------
word_count = defaultdict(int)
for word in "the quick brown fox jumps over the lazy dog".split():
word_count[word] += 1
print(dict(word_count))
grouped = defaultdict(list)
data = [("math", 90), ("eng", 85), ("math", 92), ("eng", 88)]
for subject, score in data:
grouped[subject].append(score)
print(dict(grouped)) # {'math': [90, 92], 'eng': [85, 88]}
# ----------------------------------------------------------------
# 9. Counter
# ----------------------------------------------------------------
from collections import Counter
text = "mississippi"
c = Counter(text)
print(c) # Counter({'s': 4, 'i': 4, 'p': 2, 'm': 1})
print(c.most_common(2)) # [('s', 4), ('i', 4)]
print(c["s"]) # 4
print(c["z"]) # 0 (no KeyError for Counter!)
# ----------------------------------------------------------------
# 10. True/1/1.0 same key
# ----------------------------------------------------------------
d = {1: "one"}
d[True] = "True" # overwrites d[1]
d[1.0] = "1.0" # overwrites again
print(d) # {1: '1.0'}
print(len(d)) # 1
# ----------------------------------------------------------------
# 11. TypedDict (Python 3.8+)
# ----------------------------------------------------------------
class Movie(TypedDict):
title: str
year: int
rating: float
# TypedDict is for static type checking only; runtime is just a dict:
m: Movie = {"title": "Inception", "year": 2010, "rating": 8.8}
print(m["title"]) # Inception
print(isinstance(m, dict)) # True
# ----------------------------------------------------------------
# 12. ChainMap (lookup through multiple dicts)
# ----------------------------------------------------------------
defaults = {"color": "blue", "user": "guest"}
environ = {"user": "alice"}
config = ChainMap(environ, defaults)
print(config["user"]) # alice (environ wins)
print(config["color"]) # blue (from defaults)
print(list(config.keys())) # ['user', 'color']
| Section | Topic | URL |
| §3.2 | Mapping types | https://docs.python.org/3/reference/datamodel.html#mapping-types |
| §6.2.6 | Dictionary displays | https://docs.python.org/3/reference/expressions.html#dictionary-displays |
| §3.3.7 | __missing__ | https://docs.python.org/3/reference/datamodel.html#object.missing |
dict | Built-in dict type | https://docs.python.org/3/library/stdtypes.html#mapping-types-dict |
collections | OrderedDict, defaultdict | https://docs.python.org/3/library/collections.html |
collections.abc | MutableMapping ABC | https://docs.python.org/3/library/collections.abc.html |
typing.TypedDict | Typed dict | https://docs.python.org/3/library/typing.html#typing.TypedDict |
| PEP 274 | Dict comprehensions | https://peps.python.org/pep-0274/ |
| PEP 448 | Unpacking generalizations | https://peps.python.org/pep-0448/ |
| PEP 584 | Dict merge operators \| | https://peps.python.org/pep-0584/ |
| PEP 585 | dict[K, V] subscript | https://peps.python.org/pep-0585/ |
| PEP 589 | TypedDict | https://peps.python.org/pep-0589/ |
| PEP 655 | Required/NotRequired in TypedDict | https://peps.python.org/pep-0655/ |
| PEP 3106 | Views for dict methods | https://peps.python.org/pep-3106/ |