typedmultidict
Pure-Python typed ordered multi-value dictionary with a case-insensitive variant.
A MultiDict keeps every (key, value) pair of a query string or a header block, in
order, and still behaves as a normal Mapping over the first value of each key.
CIMultiDict compares keys case-insensitively, as HTTP field names require (RFC 9110,
section 5.1). Zero dependencies, mypy --strict clean, and every operation costs time in
proportion to the pairs it touches.
Read the guide for every rule and the comparison with other implementations, and the changelog for what changed between releases.
Install
pip install typedmultidict
Usage
A MultiDict[K, V] is a normal mapping over the first value of each key, plus methods
that see every value. Pairs keep their insertion order across keys.
from typedmultidict import MultiDict
params: MultiDict[str, str] = MultiDict([("tag", "python"), ("page", "2")])
params.add("tag", "typing")
print(params["tag"]) # python
print(params.getall("tag")) # ['python', 'typing']
print(len(params), len(params.allitems())) # 2 3
print(list(params.allitems())) # [('tag', 'python'), ('page', '2'), ('tag', 'typing')]
print(dict(params)) # {'tag': 'python', 'page': '2'}
print(params.to_dict()) # {'tag': ['python', 'typing'], 'page': ['2']}
Assigning replaces every value of a key in the position of its first one; removing can take the first value, every value, or the last pair in order:
from typedmultidict import MultiDict
md = MultiDict([("a", 1), ("b", 2), ("a", 3), ("a", 4)])
md["a"] = 9
print(md) # MultiDict([('a', 9), ('b', 2)])
md.extend([("a", 5), ("c", 6)])
print(md.popone("a"), md.getall("a")) # 9 [5]
print(md.popall("a"), "a" in md) # [5] False
print(md.popitem(), md) # ('c', 6) MultiDict([('b', 2)])
print(md.getone("missing", "default")) # default
extend appends, merge only adds keys that are not present yet, and update replaces
the values of each incoming key in place (extra new values go to the end, extra old
values are dropped):
from typedmultidict import MultiDict
md = MultiDict([("a", 1), ("b", 2), ("a", 3)])
md.merge([("a", 0), ("c", 4)])
print(md) # MultiDict([('a', 1), ('b', 2), ('a', 3), ('c', 4)])
md.update([("a", 7), ("a", 8), ("a", 9)], b=0)
print(md) # MultiDict([('a', 7), ('b', 0), ('a', 8), ('c', 4), ('a', 9)])
CIMultiDict[V] has str keys compared case-insensitively. Every pair keeps the spelling
it was added with, and keys are listed in the spelling of their first pair:
from typedmultidict import CIMultiDict
headers: CIMultiDict[str] = CIMultiDict()
headers.add("Set-Cookie", "a=1")
headers.add("set-cookie", "b=2")
headers.add("Content-Type", "text/html")
print(headers["SET-COOKIE"]) # a=1
print(headers.getall("set-cookie")) # ['a=1', 'b=2']
print(list(headers)) # ['Set-Cookie', 'Content-Type']
print(5 in headers) # False
Views from allitems() and allvalues() follow the mapping. Changing the mapping while
iterating over it, or over any of its views, raises RuntimeError. As in multidict,
every write counts, even replacing a value in place (which dict allows):
from typedmultidict import MultiDict
md = MultiDict([("a", 1)])
pairs = md.allitems()
md.add("a", 2)
print(len(pairs), ("a", 2) in pairs) # 2 True
try:
for key in md:
md.add("b", 3)
except RuntimeError as error:
print(error) # multi-dict changed during iteration
Equality, copies and pickles:
import pickle
from typedmultidict import CIMultiDict, MultiDict
print(MultiDict([("a", 1), ("b", 2)]) == MultiDict([("b", 2), ("a", 1)])) # False
print(MultiDict([("a", 1), ("b", 2)]) == {"b": 2, "a": 1}) # True
print(MultiDict([("a", 1), ("a", 2)]) == {"a": 1}) # False
print(CIMultiDict([("A", 1)]) == MultiDict([("a", 1)])) # True
headers = CIMultiDict([("X-Id", "1")])
print(pickle.loads(pickle.dumps(headers))) # CIMultiDict([('X-Id', '1')])
print(headers.copy() == headers, headers.copy() is headers) # True False
API
| Name | What it does |
|---|---|
MultiDict[K, V](initial=None, /, **kwargs) |
Ordered multi-value mapping. initial is a multi-dict (every pair is copied), a mapping or header object (read through items(), else keys()) or an iterable of pairs; keyword arguments add string-keyed pairs after it, as in dict(). K must be hashable. |
CIMultiDict[V](initial=None, /, **kwargs) |
MultiDict[str, V] whose keys compare with str.lower. A non-str key raises TypeError when added and is absent when looked up. |
md[key], get, in, keys, items, values, len(), iteration |
The Mapping view: first value of each key, keys once each. |
md[key] = value |
Replaces every value of key with one, in the place of the first. |
del md[key], pop(key[, default]) |
Remove every value of key (pop returns the first). |
setdefault(key[, default]) |
First value of key, or adds default. |
add(key, value) |
Appends one more pair. |
extend(other=(), /, **kwargs) |
Appends every pair of other, then the keyword pairs. |
merge(other=(), /, **kwargs) |
Appends the pairs (keyword pairs last) whose key is not present yet. |
update(other=(), /, **kwargs) |
Replaces the values of each incoming key in place. |
getone(key[, default]) |
First value, else default, else KeyError. |
getall(key[, default]) |
New list of every value of key; if absent, default when given, else an empty list. |
popone(key[, default]) |
Removes and returns the first value only. |
popall(key[, default]) |
Removes and returns every value as a list. |
popitem() |
Removes and returns the last pair in order (KeyError when empty). |
allitems(), allvalues() |
Live views of every pair or value (AllItemsView, AllValuesView); len() counts pairs. |
to_dict() |
New dict from each key to a new list of its values. |
copy(), clear() |
Shallow copy of the same class; remove everything. |
Bulk input (initial, extend, merge, update) is read and checked in full first: an
element that is not a pair raises TypeError, one of the wrong length raises
ValueError, both with its position, and the mapping is left unchanged. A source with
items() is read through it, so an http.client or email message keeps every field of
a repeated name. Lookups cost the same whatever the size, and so do add, popone and
popitem on average; other removals cost time in proportion to the pairs they remove.
Compared with multidict and Werkzeug
The method names and their behaviour follow the multidict package, except where
that would break the Mapping contract: len() counts keys (use
len(md.allitems()) for pairs), and items and values show the first value of each
key, as Werkzeug's MultiDict does. getall returns an empty list for a missing key
instead of raising, and pop removes every value of the key.
A multi-dict is written in pure Python, so unlike a dict it is not thread-safe: share
one between threads only behind a lock that covers reads as well as writes. Without one,
concurrent writes can raise RuntimeError and leave it inconsistent. Equality between a plain and a case-insensitive multi-dict folds case, so it is not
transitive across the two kinds. Any other mapping, a multidict.MultiDict included, is
compared as a plain mapping.
Why
HTTP headers and query strings routinely carry repeated keys, so a mapping that keeps every value is a common need. The established options each make a trade-off:
multidict(aio-libs) is fast and typed, butlen(),items()andvalues()count every pair, so it is not aMappingof keys;getallraises for a missing key andpopremoves a single value. Version 7.1.0 ships 169 compiled wheels next to its pure-Python one.- Werkzeug's
MultiDictcomes with the Werkzeug WSGI library and stores values per key, so the order of pairs across keys is lost (items(multi=True)groups them by key), and itsupdateappends. orderedmultidictdepends onsix; requests'CaseInsensitiveDictkeeps one value per name;email.messageappends on assignment.
typedmultidict is a single, standalone implementation with zero runtime dependencies
and py.typed: a true MutableMapping, the full multi-value API, pair order kept exactly,
and behaviour checked call by call against multidict, Werkzeug, requests and the standard
library (see the guide).
License
MIT. See LICENSE.
Links
- Website: https://lianes8studio.com
- GitHub: https://github.com/JaviLianes8
- LinkedIn: https://www.linkedin.com/in/jlianes/
Metadata
Release files for typedmultidict 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| typedmultidict-1.0.0.tar.gz | 131.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| typedmultidict-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 152.1 kB
Release files / typedmultidict-1.0.0.tar.gz
| Download URL | typedmultidict-1.0.0.tar.gz |
|---|---|
| Size | 131.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
91029737ad74a34a964da91d5608183ed9f4b837492e44676041141e3c64410e
|
|
BLAKE2b-256 checksum How to use checksums |
3a5a460cf5e740e683f8bb1340b8534626312c8ac2dea4904499c227d9cac17e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.2
|
Release files / typedmultidict-1.0.0-py3-none-any.whl
| Download URL | typedmultidict-1.0.0-py3-none-any.whl |
|---|---|
| Size | 20.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9b31cf672003f70c121c699e021e62e7726a7cbe6b7c803c87538ab330c1f8fa
|
|
BLAKE2b-256 checksum How to use checksums |
bbf04866d1014de9f4c06f3005755dde0f50134e56891e7bdec8c7b492b699b5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.2
|