multidict
multidict is a mapping implementation that allows multiple values per key, mirroring the semantics of HTTP headers and query strings. It provides four concrete classes — MultiDict (mutable), MultiDictProxy (immutable view), CIMultiDict (case-insensitive mutable), and CIMultiDictProxy (case-insensitive immutable view) — plus the istr helper for pre-folded string keys. Current version is 6.7.1 (released January 2026). The library ships an optional C extension; release cadence is frequent patch releases with periodic minor releases adding features such as the new merge() method (6.6.0) and a hash-table backend replacing the previous O(N) array (6.5.0).
Warnings
- gotcha d['key'] returns only the FIRST value for a duplicated key. Iterating .keys() yields ALL keys including duplicates. Use getall('key') to fetch every value.
- gotcha d['key'] = value replaces ALL existing entries for that key with a single entry, not just the first. Use d.add('key', value) to append without clobbering existing values.
- breaking 6.5.0 switched the internal storage from a linear array (O(N) lookups) to a hash table (O(1) lookups). MultiDict.add() and extend() are now 25-50% slower; single lookups are 25-50% faster and bulk operations are 2-3x faster. Code relying on iteration order of keys() after deletions may observe subtly different compaction behaviour.
- breaking MultiDictProxy and CIMultiDictProxy raise TypeError if passed a plain dict or any non-MultiDict mapping. They only accept the matching MultiDict subclass.
- gotcha Setting MULTIDICT_NO_EXTENSIONS=1 at runtime forces the pure-Python implementation even when the C extension wheel is installed. The pure-Python path is roughly 20-50x slower. This env var also suppresses C extension compilation at build time if set during pip install.
- deprecated Directly importing from private submodules multidict._multidict (C extension) or multidict._multidict_py (pure Python) is not part of the public API and can break across patch releases. The internal _Impl class was removed from the pure-Python implementation in 6.4.x.
- gotcha CIMultiDict stores and returns keys as istr (a str subclass with preserved original case), not plain str. Identity or strict type checks against str will pass since istr derives from str, but isinstance checks for exactly str may differ from expectations when iterating keys.
Install
-
pip install multidict -
MULTIDICT_NO_EXTENSIONS=1 pip install multidict
Imports
- MultiDict
from multidict import MultiDict
- CIMultiDict
from multidict import CIMultiDict
- MultiDictProxy
from multidict import MultiDictProxy
- CIMultiDictProxy
from multidict import CIMultiDictProxy
- istr
from multidict import istr
- getall
d.getall('key')
Quickstart
from multidict import MultiDict, MultiDictProxy, CIMultiDict, istr
# --- Mutable multi-valued dict ---
d = MultiDict([('a', 1), ('b', 2), ('a', 3)])
assert d['a'] == 1 # returns FIRST value only
assert d.getall('a') == [1, 3] # all values for key
d.add('b', 99) # append without replacing
assert len(d) == 4
# --- set() replaces ALL entries for that key ---
d['a'] = 99
assert d.getall('a') == [99]
# --- Read-only proxy (dynamic view) ---
proxy = MultiDictProxy(d)
assert proxy['b'] == 2
d.add('b', 42)
assert proxy.getall('b') == [2, 99, 42] # proxy reflects changes
# --- Case-insensitive dict ---
ci = CIMultiDict(Content_Type='application/json')
assert 'content-type' in ci
assert ci['CONTENT-TYPE'] == 'application/json'
# --- istr for pre-folded keys (performance) ---
HDR = istr('Content-Type')
assert ci[HDR] == 'application/json'
# --- merge (6.6+): copies key only if not already present ---
base = MultiDict(a=1)
base.merge(MultiDict(a=99, b=2))
assert base['a'] == 1 # not overwritten
assert base['b'] == 2