A multidict is a dictionary-like mapping that can hold several values for the same key while keeping all pairs in insertion order. Python has no built-in multidict type, and the third-party multidict library, which aiohttp and yarl use for HTTP headers and query strings, provides MultiDict and its case-insensitive variant CIMultiDict.
from multidict import MultiDict
md = MultiDict()
md["key"] = "value"
md.add("key", "another_value") # a second value for the same key
first = md["key"] # 'value'
values = md.getall("key") # ['value', 'another_value']
md.update({"new_key": "new_value"}) # adds or replaces new_key
del md["key"] # removes every value of key
popped = md.popall("new_key") # ['new_value']
md.clear()
After installing the library, we walk through creating, adding, reading, updating and deleting values, with the return value of every method. We also cover case-insensitive keys, read-only proxies and the plain Python alternatives. The code was tested with multidict 7.0 on Python 3.14.
1. What is a Multidict?
In a regular dictionary, each key has one value, and assigning again replaces it. A multidict stores every pair we add, so one key can map to many values, and the pairs keep the order in which they were added.
colors = {"Monday": "Blue", "Tuesday": "Red"}
colors["Monday"] = "Yellow" # replaces Blue
plain = colors # {'Monday': 'Yellow', 'Tuesday': 'Red'}
With a multidict, Monday keeps both colors.
from multidict import MultiDict
colors = MultiDict()
colors.add("Monday", "Blue")
colors.add("Monday", "Yellow")
colors.add("Tuesday", "Red")
monday = colors.getall("Monday") # ['Blue', 'Yellow']
keys = list(colors.keys()) # ['Monday', 'Monday', 'Tuesday']
count = len(colors) # 3, the number of pairs
len() and keys() count pairs, not distinct keys. A key with two values appears twice in keys(), so set(md.keys()) gives the distinct keys.
2. Installation and Import
We install the package from PyPI with pip.
pip install multidict
The package exports four classes. MultiDict and CIMultiDict are mutable, and MultiDictProxy and CIMultiDictProxy are read-only views of them.
from multidict import MultiDict, CIMultiDict, MultiDictProxy
3. Basic CRUD Operations on a MultiDict
The methods follow the dict API where possible, and add methods whose names end in one or all for the cases where a key has several values.
3.1. Creating a Multidict
The constructor accepts a list of pairs, a mapping, keyword arguments or another multidict. Repeated keys in the list of pairs are all kept.
empty = MultiDict()
with_items = MultiDict([("key", "value1"), ("key", "value2"), ("new_key", "value3")])
from_kwargs = MultiDict(page="2", sort="date")
pairs = list(with_items.items())
# [('key', 'value1'), ('key', 'value2'), ('new_key', 'value3')]
3.2. Adding Items to MultiDict
Item assignment replaces all values of a key, and only add() and extend() append. This is the most common source of lost values.
| Method | Description | Return value |
|---|---|---|
| md[key] = value | Removes every value of key and stores one value | None |
| add(key, value) | Appends a pair, keeping existing values | None |
| extend(pairs or mapping) | Appends every pair | None |
| setdefault(key, default=None) | Returns the first value of key, or inserts default and returns it | The first value or default |
md = MultiDict()
md["key"] = "value"
md.add("key", "another_value")
md.extend([("key", "third"), ("tag", "x")])
first = md.setdefault("key", "default") # 'value', key exists
inserted = md.setdefault("key_default", "d") # 'd', inserted
all_values = md.getall("key") # ['value', 'another_value', 'third']
md["key"] = "only"
after_assign = md.getall("key") # ['only']
3.3. Accessing Values in Multidict
Reading methods return either the first value or all values of a key, and differ in how they handle a missing key.
| Method | Description | Return value |
|---|---|---|
| md[key] | First value | The value; KeyError if missing |
| get(key, default=None) | First value | The value or default |
| getone(key[, default]) | First value | The value, default, or KeyError if no default |
| getall(key[, default]) | All values in order | A new list, default, or KeyError if no default |
Consider a multidict containing three fruits under one key.
md = MultiDict()
md.add("fruit", "apple")
md.add("fruit", "banana")
md.add("fruit", "orange")
by_index = md["fruit"] # 'apple'
by_get = md.get("fruit", "No fruit") # 'apple'
by_getone = md.getone("fruit", "No fruit") # 'apple'
by_getall = md.getall("fruit") # ['apple', 'banana', 'orange']
missing = md.getall("veg", []) # []
Calling getall(“veg”) without a default raises KeyError: ‘veg’, so a default of [] makes loops over optional keys safe.
3.4. Updating Value of an Existing Key
The update() method replaces the values of the keys it receives and adds new keys at the end. A key with several values is reduced to the new value, while keys not passed to update() stay unchanged. The merge() method, available in multidict 7.0, adds only the keys that are missing and leaves existing ones alone.
md = MultiDict([("a", 1), ("b", 2), ("a", 3)])
md.update({"a": 9})
updated = list(md.items()) # [('a', 9), ('b', 2)]
md2 = MultiDict([("a", 1)])
md2.merge({"a": 2, "z": 3})
merged = list(md2.items()) # [('a', 1), ('z', 3)]
No built-in method changes a single value among several values of one key. We read all values with popall(), change the list and add the values back with extend(). Passing the list to update() instead would store the whole list as one value.
md = MultiDict([("key", "value"), ("key", "another_value")])
values = md.popall("key")
values[values.index("another_value")] = "new_value"
md.extend(("key", v) for v in values)
result = md.getall("key") # ['value', 'new_value']
Because the pairs are added again, the key moves to the end of the multidict. When the position matters, we rebuild the whole multidict from items().
3.5. Deleting Values from MultiDict
We delete entries with the del statement, the pop methods or clear().
| Method | Description | Return value |
|---|---|---|
| del md[key] | Removes every value of key | None; KeyError if missing |
| pop(key[, default]), popone(key[, default]) | Removes the first value of key | That value or default; KeyError if missing and no default |
| popall(key[, default]) | Removes every value of key | A list of the values, or default |
| popitem() | Removes the last added pair | A (key, value) tuple; KeyError if empty |
| clear() | Removes all pairs | None |
md = MultiDict([("a", 1), ("b", 2), ("a", 3)])
first_a = md.popone("a") # 1, md has b=2 and a=3
last_pair = md.popitem() # ('a', 3)
fallback = md.pop("x", "default") # 'default'
md.add("c", 4)
del md["c"]
left = list(md.items()) # [('b', 2)]
4. Case-Insensitive Keys and Read-Only Proxies
HTTP header names are case-insensitive, so CIMultiDict treats Content-Type and content-type as the same key while keeping the original spelling. A proxy wraps a multidict and blocks changes, which is how aiohttp exposes request headers.
from multidict import CIMultiDict, MultiDict, MultiDictProxy
headers = CIMultiDict()
headers.add("Set-Cookie", "a=1")
headers.add("set-cookie", "b=2")
cookies = headers.getall("SET-COOKIE") # ['a=1', 'b=2']
read_only = MultiDictProxy(MultiDict(page="2"))
>>> read_only["page"] = "3"
TypeError: 'multidict._multidict.MultiDictProxy' object does not support item assignment
5. MultiDict vs defaultdict(list) and Other Options
A multidict is not the only way to map a key to several values. A plain dictionary of lists works for grouping, but it loses the order of pairs across keys.
| Option | Keeps order of all pairs | Case-insensitive option | Install needed |
|---|---|---|---|
| multidict.MultiDict | Yes | Yes, CIMultiDict | Yes |
| collections.defaultdict(list) | No, order only per key | No | No |
| urllib.parse.parse_qs() | No, returns a dict of lists | No | No |
| werkzeug.datastructures.MultiDict | Yes | Separate Headers class | Yes, comes with Flask |
from collections import defaultdict
groups = defaultdict(list)
for day, color in [("Monday", "Blue"), ("Tuesday", "Red"), ("Monday", "Yellow")]:
groups[day].append(color)
grouped = dict(groups) # {'Monday': ['Blue', 'Yellow'], 'Tuesday': ['Red']}
We pick defaultdict(list) for grouping data inside application code and a multidict when the data is a sequence of pairs that must round-trip, such as headers, form fields and query strings. The Python dictionary tutorial covers the regular dict methods.
6. Real-World Example: Parsing a Query String
A URL such as ?tag=py&tag=web&page=2 repeats the tag parameter. Loading the pairs from parse_qsl() into a multidict keeps every tag and the original order.
from urllib.parse import parse_qsl, urlencode
from multidict import MultiDict
params = MultiDict(parse_qsl("tag=py&tag=web&page=2"))
tags = params.getall("tag") # ['py', 'web']
page = int(params.get("page", "1")) # 2
params["page"] = "3"
next_query = urlencode(list(params.items())) # 'tag=py&tag=web&page=3'
7. MultiDict FAQs
Converting and comparing multidicts behaves differently from plain dicts.
7.1. How do we convert a MultiDict to a regular dict?
Calling dict(md) keeps only the first value of each key. To keep every value, we build a dict of lists.
md = MultiDict([("tag", "a"), ("tag", "b")])
first_only = dict(md) # {'tag': 'a'}
all_values = {k: md.getall(k) for k in set(md.keys())} # {'tag': ['a', 'b']}
7.2. Does Python have a built-in multidict?
No. The standard library has defaultdict(list) for grouping, and email.message.Message and http.client.HTTPMessage handle repeated headers internally, but there is no general multidict type.
7.3. Is a MultiDict equal to a dict with the same pairs?
Yes, when every key has one value. MultiDict(a=1) == {“a”: 1} returns True, and the comparison of two multidicts also checks the order of the pairs.
8. Conclusion
With a multidict, one key keeps several values in insertion order. add() and extend() append values, md[key] and getone() read the first value and getall() reads all of them. Keep in mind that item assignment and update() replace every value of a key. CIMultiDict handles case-insensitive names such as HTTP headers, and defaultdict(list) is the standard-library option for simple grouping.
9. References
Happy Learning !!