Skip to content

MultiDict

The main multicollections module provides the MultiDict class, a fully generic dictionary that allows multiple values with the same key while preserving insertion order.

Overview

MultiDict is a mutable multi-mapping that inherits from multicollections.abc.MutableMultiMapping. It provides all the functionality you'd expect from a dictionary, plus the ability to store multiple values for the same key.

Key Features

  • Multiple values per key: Store multiple values for the same key
  • Insertion order preservation: Keys and values maintain their insertion order
  • Dictionary-like interface: Familiar API similar to built-in dict
  • Abstract base class compliance: Inherits from MutableMultiMapping

Quick Start

from multicollections import MultiDict

# Create a MultiDict
md = MultiDict([('a', 1), ('b', 2), ('a', 3)])

# Access first value for a key
print(md['a'])  # 1

# Add more values
md.add('b', 4)

# Set a single value (replaces all existing values)
md['a'] = 999

# Iterate over all key-value pairs
for key, value in md.items():
    print(f"{key}: {value}")

API Reference

multicollections

Fully generic MultiDict class.

MultiDict

Bases: MutableMultiMapping[_K, _V]

A fully generic dictionary that allows multiple values with the same key.

Preserves insertion order.

Source code in multicollections/__init__.py
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
class MultiDict(MutableMultiMapping[_K, _V]):  # noqa: PLW1641
    """A fully generic dictionary that allows multiple values with the same key.

    Preserves insertion order.
    """

    def __init__(
        self,
        iterable: SupportsKeysAndGetItem[_K, _V] | Iterable[tuple[_K, _V]] = (),
        /,
        **kwargs: _V,
    ) -> None:
        """Create a MultiDict."""
        self._items: list[tuple[_K, _V]] = list(_yield_items(iterable, **kwargs))
        self._key_indices: dict[_K, list[int]] = {}

        # Build indices in one pass for better performance
        if self._items:
            self._rebuild_indices()

    @override
    @with_default
    def getall(self, key: _K, /) -> list[_V]:
        """Get all values for a key.

        Raises a `KeyError` if the key is not found and no default is provided.
        """
        ret = [self._items[i][1] for i in self._key_indices.get(key, [])]
        if not ret:
            raise KeyError(key)
        return ret

    @override
    @with_default
    def getone(self, key: _K, /) -> _V:
        """Get the first value for a key.

        Raises a `KeyError` if the key is not found and no default is provided.
        """
        if (indices := self._key_indices.get(key)) is None:
            raise KeyError(key)
        return self._items[indices[0]][1]

    @override
    def __getitem__(self, key: _K, /) -> _V:
        """Get the first value for a key.

        Raises a `KeyError` if the key is not found.
        """
        if (indices := self._key_indices.get(key)) is None:
            raise KeyError(key)
        return self._items[indices[0]][1]

    @override
    def __contains__(self, key: object, /) -> bool:
        """Check if a key exists in the multi-mapping.

        This is optimized to directly check the key indices without
        calling __getitem__, avoiding exception handling overhead.
        """
        return key in self._key_indices

    @overload
    def get(self, key: _K, /) -> _V | None: ...

    @overload
    def get(self, key: _K, default: _D, /) -> _V | _D: ...

    @override
    def get(self, key: _K, default: _D | None = None, /) -> _V | _D | None:
        """Get the first value for a key, or a default value if not found.

        This is optimized to directly check the key indices without
        calling __getitem__, avoiding exception handling overhead.
        """
        if (indices := self._key_indices.get(key)) is None:
            return default
        return self._items[indices[0]][1]

    @overload
    def setdefault(self, key: _K, /) -> _V | None: ...

    @overload
    def setdefault(self, key: _K, default: _D, /) -> _V | _D: ...

    @override
    def setdefault(self, key: _K, default: _D | None = None, /) -> _V | _D | None:
        """Get the first value for a key, or set and return a default if not found.

        This is optimized to perform a single lookup in the key indices,
        rather than calling __getitem__ and __setitem__ separately.
        """
        if (indices := self._key_indices.get(key)) is not None:
            # Key exists, return its first value
            return self._items[indices[0]][1]
        # Key doesn't exist, add it with the default value
        self.add(key, default)
        return default

    @override
    def __setitem__(self, key: _K, value: _V, /) -> None:
        """Set the value for a key.

        Replaces the first value for a key if it exists; otherwise, it adds a new item.
        Any other items with the same key are removed.
        """
        if key in self._key_indices:
            # Key exists, replace first occurrence and remove others
            indices = self._key_indices[key]
            first_index = indices[0]

            # Update the first occurrence
            self._items[first_index] = (key, value)

            if len(indices) > 1:
                # Remove duplicates efficiently by marking items as None and filtering
                for idx in indices[1:]:
                    self._items[idx] = None  # ty: ignore[invalid-assignment]

                # Filter out None items and rebuild indices
                self._items = [item for item in self._items if item is not None]
                self._rebuild_indices()
        else:
            # Key doesn't exist, add it
            self.add(key, value)

    def _rebuild_indices(self) -> None:
        """Rebuild the key indices after items list has been modified."""
        self._key_indices = {}
        for i, (key, _) in enumerate(self._items):
            if (indices_list := self._key_indices.get(key)) is None:
                self._key_indices[key] = indices_list = []
            indices_list.append(i)

    @override
    def add(self, key: _K, value: _V, /) -> None:
        """Add a new value for a key."""
        index = len(self._items)
        self._items.append((key, value))
        if (indices_list := self._key_indices.get(key)) is None:
            self._key_indices[key] = indices_list = []
        indices_list.append(index)

    @override
    @with_default
    def popone(self, key: _K, /) -> _V:
        """Remove and return the first value for a key."""
        if (indices := self._key_indices.get(key)) is None:
            raise KeyError(key)

        first_index = indices[0]
        value = self._items[first_index][1]

        # Mark the first item for removal
        self._items[first_index] = None  # ty: ignore[invalid-assignment]

        # Filter out None items and rebuild indices
        self._items = [item for item in self._items if item is not None]
        self._rebuild_indices()

        return value

    @override
    def __delitem__(self, key: _K, /) -> None:
        """Remove all values for a key.

        Raises a `KeyError` if the key is not found.
        """
        if (indices_to_remove := self._key_indices.get(key)) is None:
            raise KeyError(key)

        # Mark items for removal
        for idx in indices_to_remove:
            self._items[idx] = None  # ty: ignore[invalid-assignment]

        # Filter out None items and rebuild indices
        self._items = [item for item in self._items if item is not None]
        self._rebuild_indices()

    @override
    def __iter__(self) -> Iterator[_K]:
        """Return an iterator over the keys, in insertion order.

        Keys with multiple values will be yielded multiple times.
        """
        return (k for k, _ in self._items)

    @override
    def __len__(self) -> int:
        """Return the total number of items."""
        return len(self._items)

    @override
    def clear(self) -> None:
        """Remove all items from the multi-mapping."""
        self._items.clear()
        self._key_indices.clear()

    def _collect_update_items(
        self,
        all_items: list[tuple[_K, _V]],
        existing_keys: set[_K],
    ) -> tuple[dict[_K, list[_V]], list[tuple[_K, _V]]]:
        """Separate items into updates and additions."""
        updates_by_key: dict[_K, list[_V]] = {}  # key -> list of values to replace with
        additions = []  # list of (key, value) for new keys

        for key, value in all_items:
            if key in existing_keys:
                if (values_list := updates_by_key.get(key)) is None:
                    updates_by_key[key] = values_list = []
                values_list.append(value)
            else:
                additions.append((key, value))

        return updates_by_key, additions

    def _process_updates(self, updates_by_key: dict[_K, list[_V]]) -> None:
        """Process updates efficiently by batch removing and adding."""
        # Mark items for removal that need to be replaced
        items_to_remove = set()
        for key in updates_by_key:
            items_to_remove.update(self._key_indices[key])

        # Mark items for removal
        for idx in items_to_remove:
            self._items[idx] = None  # ty: ignore[invalid-assignment]

        # Filter out None items
        self._items = [item for item in self._items if item is not None]

        # Add updated items (all values for each key)
        for key, values in updates_by_key.items():
            for value in values:
                self._items.append((key, value))

    @override
    def update(
        self,
        other: SupportsKeysAndGetItem[_K, _V] | Iterable[tuple[_K, _V]] = (),
        /,
        **kwargs: _V,
    ) -> None:
        """Update the multi-mapping with items from another object.

        This replaces existing values for keys found in the other object.
        This is optimized for batch operations.
        """
        # Collect all items first
        all_items = list(_yield_items(other, **kwargs))

        if not all_items:
            return

        # Get existing keys once for efficiency
        existing_keys = set(self._key_indices.keys())

        # Separate items into updates and additions
        updates_by_key, additions = self._collect_update_items(all_items, existing_keys)

        # Process updates efficiently
        if updates_by_key:
            self._process_updates(updates_by_key)

        # Add new items
        if additions:
            self._items.extend(additions)

        # Rebuild indices once at the end
        if updates_by_key or additions:
            self._rebuild_indices()

    @override
    def merge(
        self,
        other: SupportsKeysAndGetItem[_K, _V] | Iterable[tuple[_K, _V]] = (),
        /,
        **kwargs: _V,
    ) -> None:
        """Merge another object into the multi-mapping.

        Keys from `other` that already exist in the multi-mapping will not be added.
        This is optimized for batch operations.
        """
        # Get existing keys once for efficiency
        existing_keys = set(self._key_indices.keys())

        # Collect all items and filter out existing keys
        new_items = [
            (key, value)
            for key, value in _yield_items(other, **kwargs)
            if key not in existing_keys
        ]

        if not new_items:
            return

        # Add all items to the list at once
        start_index = len(self._items)
        self._items.extend(new_items)

        # Update indices incrementally for better performance
        for i, (key, _) in enumerate(new_items, start_index):
            if (indices_list := self._key_indices.get(key)) is None:
                self._key_indices[key] = indices_list = []
            indices_list.append(i)

    @override
    def extend(
        self,
        other: SupportsKeysAndGetItem[_K, _V] | Iterable[tuple[_K, _V]] = (),
        /,
        **kwargs: _V,
    ) -> None:
        """Extend the multi-mapping with items from another object.

        This is optimized for batch operations to avoid rebuilding indices
        multiple times.
        """
        # Collect all new items first
        new_items = list(_yield_items(other, **kwargs))

        if not new_items:
            return

        # Add all items to the list at once
        start_index = len(self._items)
        self._items.extend(new_items)

        # Update indices incrementally for better performance
        for i, (key, _) in enumerate(new_items, start_index):
            if (indices_list := self._key_indices.get(key)) is None:
                self._key_indices[key] = indices_list = []
            indices_list.append(i)

    def copy(self) -> MultiDict[_K, _V]:
        """Return a shallow copy of the MultiDict."""
        new_md = MultiDict.__new__(MultiDict)
        new_md._items = self._items.copy()  # noqa: SLF001
        new_md._key_indices = {k: v.copy() for k, v in self._key_indices.items()}  # noqa: SLF001
        return new_md

    @override
    def __eq__(self, other: object) -> bool:  # noqa: PLR0911
        """Check equality with another MultiDict or mapping-like object.

        Two `MultiDict` instances (or a `MultiDict` and any `MultiMapping`) are
        considered equal if they contain the same items (including duplicates) in the
        same order.

        For comparison with another `Mapping` object, it is equal if they are the same
        length and for each item in the `MultiDict`, the corresponding key in the
        `Mapping` has the same value.
        """
        if isinstance(other, MultiDict):
            return self._items == other._items
        if isinstance(other, MultiMapping):
            return len(self._items) == len(other) and all(
                i1 == i2 for i1, i2 in zip(self._items, other.items())
            )
        if isinstance(other, Mapping):
            if len(self) != len(other):
                return False
            try:
                for k, v in self._items:
                    if other[k] != v:  # ty: ignore[invalid-argument-type]
                        return False
            except KeyError:
                return False
            return True
        return NotImplemented

    @override
    def __repr__(self) -> str:
        """Return a string representation of the MultiDict."""
        return f"{self.__class__.__name__}({list(self._items)!r})"

__contains__(key: object) -> bool

Check if a key exists in the multi-mapping.

This is optimized to directly check the key indices without calling getitem, avoiding exception handling overhead.

Source code in multicollections/__init__.py
78
79
80
81
82
83
84
85
@override
def __contains__(self, key: object, /) -> bool:
    """Check if a key exists in the multi-mapping.

    This is optimized to directly check the key indices without
    calling __getitem__, avoiding exception handling overhead.
    """
    return key in self._key_indices

__delitem__(key: _K) -> None

Remove all values for a key.

Raises a KeyError if the key is not found.

Source code in multicollections/__init__.py
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
@override
def __delitem__(self, key: _K, /) -> None:
    """Remove all values for a key.

    Raises a `KeyError` if the key is not found.
    """
    if (indices_to_remove := self._key_indices.get(key)) is None:
        raise KeyError(key)

    # Mark items for removal
    for idx in indices_to_remove:
        self._items[idx] = None  # ty: ignore[invalid-assignment]

    # Filter out None items and rebuild indices
    self._items = [item for item in self._items if item is not None]
    self._rebuild_indices()

__eq__(other: object) -> bool

Check equality with another MultiDict or mapping-like object.

Two MultiDict instances (or a MultiDict and any MultiMapping) are considered equal if they contain the same items (including duplicates) in the same order.

For comparison with another Mapping object, it is equal if they are the same length and for each item in the MultiDict, the corresponding key in the Mapping has the same value.

Source code in multicollections/__init__.py
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
@override
def __eq__(self, other: object) -> bool:  # noqa: PLR0911
    """Check equality with another MultiDict or mapping-like object.

    Two `MultiDict` instances (or a `MultiDict` and any `MultiMapping`) are
    considered equal if they contain the same items (including duplicates) in the
    same order.

    For comparison with another `Mapping` object, it is equal if they are the same
    length and for each item in the `MultiDict`, the corresponding key in the
    `Mapping` has the same value.
    """
    if isinstance(other, MultiDict):
        return self._items == other._items
    if isinstance(other, MultiMapping):
        return len(self._items) == len(other) and all(
            i1 == i2 for i1, i2 in zip(self._items, other.items())
        )
    if isinstance(other, Mapping):
        if len(self) != len(other):
            return False
        try:
            for k, v in self._items:
                if other[k] != v:  # ty: ignore[invalid-argument-type]
                    return False
        except KeyError:
            return False
        return True
    return NotImplemented

__getitem__(key: _K) -> _V

Get the first value for a key.

Raises a KeyError if the key is not found.

Source code in multicollections/__init__.py
68
69
70
71
72
73
74
75
76
@override
def __getitem__(self, key: _K, /) -> _V:
    """Get the first value for a key.

    Raises a `KeyError` if the key is not found.
    """
    if (indices := self._key_indices.get(key)) is None:
        raise KeyError(key)
    return self._items[indices[0]][1]

__init__(iterable: SupportsKeysAndGetItem[_K, _V] | Iterable[tuple[_K, _V]] = (), /, **kwargs: _V) -> None

Create a MultiDict.

Source code in multicollections/__init__.py
31
32
33
34
35
36
37
38
39
40
41
42
43
def __init__(
    self,
    iterable: SupportsKeysAndGetItem[_K, _V] | Iterable[tuple[_K, _V]] = (),
    /,
    **kwargs: _V,
) -> None:
    """Create a MultiDict."""
    self._items: list[tuple[_K, _V]] = list(_yield_items(iterable, **kwargs))
    self._key_indices: dict[_K, list[int]] = {}

    # Build indices in one pass for better performance
    if self._items:
        self._rebuild_indices()

__iter__() -> Iterator[_K]

Return an iterator over the keys, in insertion order.

Keys with multiple values will be yielded multiple times.

Source code in multicollections/__init__.py
204
205
206
207
208
209
210
@override
def __iter__(self) -> Iterator[_K]:
    """Return an iterator over the keys, in insertion order.

    Keys with multiple values will be yielded multiple times.
    """
    return (k for k, _ in self._items)

__len__() -> int

Return the total number of items.

Source code in multicollections/__init__.py
212
213
214
215
@override
def __len__(self) -> int:
    """Return the total number of items."""
    return len(self._items)

__repr__() -> str

Return a string representation of the MultiDict.

Source code in multicollections/__init__.py
397
398
399
400
@override
def __repr__(self) -> str:
    """Return a string representation of the MultiDict."""
    return f"{self.__class__.__name__}({list(self._items)!r})"

__setitem__(key: _K, value: _V) -> None

Set the value for a key.

Replaces the first value for a key if it exists; otherwise, it adds a new item. Any other items with the same key are removed.

Source code in multicollections/__init__.py
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
@override
def __setitem__(self, key: _K, value: _V, /) -> None:
    """Set the value for a key.

    Replaces the first value for a key if it exists; otherwise, it adds a new item.
    Any other items with the same key are removed.
    """
    if key in self._key_indices:
        # Key exists, replace first occurrence and remove others
        indices = self._key_indices[key]
        first_index = indices[0]

        # Update the first occurrence
        self._items[first_index] = (key, value)

        if len(indices) > 1:
            # Remove duplicates efficiently by marking items as None and filtering
            for idx in indices[1:]:
                self._items[idx] = None  # ty: ignore[invalid-assignment]

            # Filter out None items and rebuild indices
            self._items = [item for item in self._items if item is not None]
            self._rebuild_indices()
    else:
        # Key doesn't exist, add it
        self.add(key, value)

add(key: _K, value: _V) -> None

Add a new value for a key.

Source code in multicollections/__init__.py
159
160
161
162
163
164
165
166
@override
def add(self, key: _K, value: _V, /) -> None:
    """Add a new value for a key."""
    index = len(self._items)
    self._items.append((key, value))
    if (indices_list := self._key_indices.get(key)) is None:
        self._key_indices[key] = indices_list = []
    indices_list.append(index)

clear() -> None

Remove all items from the multi-mapping.

Source code in multicollections/__init__.py
217
218
219
220
221
@override
def clear(self) -> None:
    """Remove all items from the multi-mapping."""
    self._items.clear()
    self._key_indices.clear()

copy() -> MultiDict[_K, _V]

Return a shallow copy of the MultiDict.

Source code in multicollections/__init__.py
360
361
362
363
364
365
def copy(self) -> MultiDict[_K, _V]:
    """Return a shallow copy of the MultiDict."""
    new_md = MultiDict.__new__(MultiDict)
    new_md._items = self._items.copy()  # noqa: SLF001
    new_md._key_indices = {k: v.copy() for k, v in self._key_indices.items()}  # noqa: SLF001
    return new_md

extend(other: SupportsKeysAndGetItem[_K, _V] | Iterable[tuple[_K, _V]] = (), /, **kwargs: _V) -> None

Extend the multi-mapping with items from another object.

This is optimized for batch operations to avoid rebuilding indices multiple times.

Source code in multicollections/__init__.py
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
@override
def extend(
    self,
    other: SupportsKeysAndGetItem[_K, _V] | Iterable[tuple[_K, _V]] = (),
    /,
    **kwargs: _V,
) -> None:
    """Extend the multi-mapping with items from another object.

    This is optimized for batch operations to avoid rebuilding indices
    multiple times.
    """
    # Collect all new items first
    new_items = list(_yield_items(other, **kwargs))

    if not new_items:
        return

    # Add all items to the list at once
    start_index = len(self._items)
    self._items.extend(new_items)

    # Update indices incrementally for better performance
    for i, (key, _) in enumerate(new_items, start_index):
        if (indices_list := self._key_indices.get(key)) is None:
            self._key_indices[key] = indices_list = []
        indices_list.append(i)

get(key: _K, default: _D | None = None) -> _V | _D | None

get(key: _K) -> _V | None
get(key: _K, default: _D) -> _V | _D

Get the first value for a key, or a default value if not found.

This is optimized to directly check the key indices without calling getitem, avoiding exception handling overhead.

Source code in multicollections/__init__.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
@override
def get(self, key: _K, default: _D | None = None, /) -> _V | _D | None:
    """Get the first value for a key, or a default value if not found.

    This is optimized to directly check the key indices without
    calling __getitem__, avoiding exception handling overhead.
    """
    if (indices := self._key_indices.get(key)) is None:
        return default
    return self._items[indices[0]][1]

getall(key: _K) -> list[_V]

Get all values for a key.

Raises a KeyError if the key is not found and no default is provided.

Source code in multicollections/__init__.py
45
46
47
48
49
50
51
52
53
54
55
@override
@with_default
def getall(self, key: _K, /) -> list[_V]:
    """Get all values for a key.

    Raises a `KeyError` if the key is not found and no default is provided.
    """
    ret = [self._items[i][1] for i in self._key_indices.get(key, [])]
    if not ret:
        raise KeyError(key)
    return ret

getone(key: _K) -> _V

Get the first value for a key.

Raises a KeyError if the key is not found and no default is provided.

Source code in multicollections/__init__.py
57
58
59
60
61
62
63
64
65
66
@override
@with_default
def getone(self, key: _K, /) -> _V:
    """Get the first value for a key.

    Raises a `KeyError` if the key is not found and no default is provided.
    """
    if (indices := self._key_indices.get(key)) is None:
        raise KeyError(key)
    return self._items[indices[0]][1]

items() -> ItemsView[_K, _V]

Return a view of the items (key-value pairs) in the MultiMapping.

Source code in multicollections/abc.py
225
226
227
228
@override
def items(self) -> ItemsView[_K, _V]:
    """Return a view of the items (key-value pairs) in the MultiMapping."""
    return ItemsView(self)

keys() -> KeysView[_K]

Return a view of the keys in the MultiMapping.

Source code in multicollections/abc.py
220
221
222
223
@override
def keys(self) -> KeysView[_K]:
    """Return a view of the keys in the MultiMapping."""
    return KeysView(self)

merge(other: SupportsKeysAndGetItem[_K, _V] | Iterable[tuple[_K, _V]] = (), /, **kwargs: _V) -> None

Merge another object into the multi-mapping.

Keys from other that already exist in the multi-mapping will not be added. This is optimized for batch operations.

Source code in multicollections/__init__.py
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
@override
def merge(
    self,
    other: SupportsKeysAndGetItem[_K, _V] | Iterable[tuple[_K, _V]] = (),
    /,
    **kwargs: _V,
) -> None:
    """Merge another object into the multi-mapping.

    Keys from `other` that already exist in the multi-mapping will not be added.
    This is optimized for batch operations.
    """
    # Get existing keys once for efficiency
    existing_keys = set(self._key_indices.keys())

    # Collect all items and filter out existing keys
    new_items = [
        (key, value)
        for key, value in _yield_items(other, **kwargs)
        if key not in existing_keys
    ]

    if not new_items:
        return

    # Add all items to the list at once
    start_index = len(self._items)
    self._items.extend(new_items)

    # Update indices incrementally for better performance
    for i, (key, _) in enumerate(new_items, start_index):
        if (indices_list := self._key_indices.get(key)) is None:
            self._key_indices[key] = indices_list = []
        indices_list.append(i)

pop(key: _K) -> _V

Same as popone.

Source code in multicollections/abc.py
280
281
282
283
284
@with_default
@override
def pop(self, key: _K, /) -> _V:
    """Same as `popone`."""
    return self.popone(key)

popall(key: _K) -> Collection[_V]

Remove and return all values for a key.

Raises a KeyError if the key is not found and no default is provided.

Source code in multicollections/abc.py
268
269
270
271
272
273
274
275
276
277
278
@with_default
def popall(self, key: _K, /) -> Collection[_V]:
    """Remove and return all values for a key.

    Raises a `KeyError` if the key is not found and no default is provided.
    """
    ret = [self.popone(key)]
    with contextlib.suppress(KeyError):
        while True:
            ret.append(self.popone(key))
    return ret

popitem() -> tuple[_K, _V]

Remove and return a (key, value) pair.

Source code in multicollections/abc.py
286
287
288
289
290
291
@override
def popitem(self) -> tuple[_K, _V]:
    """Remove and return a (key, value) pair."""
    key = next(iter(self))
    value = self.popone(key)
    return key, value

popone(key: _K) -> _V

Remove and return the first value for a key.

Source code in multicollections/__init__.py
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
@override
@with_default
def popone(self, key: _K, /) -> _V:
    """Remove and return the first value for a key."""
    if (indices := self._key_indices.get(key)) is None:
        raise KeyError(key)

    first_index = indices[0]
    value = self._items[first_index][1]

    # Mark the first item for removal
    self._items[first_index] = None  # ty: ignore[invalid-assignment]

    # Filter out None items and rebuild indices
    self._items = [item for item in self._items if item is not None]
    self._rebuild_indices()

    return value

setdefault(key: _K, default: _D | None = None) -> _V | _D | None

setdefault(key: _K) -> _V | None
setdefault(key: _K, default: _D) -> _V | _D

Get the first value for a key, or set and return a default if not found.

This is optimized to perform a single lookup in the key indices, rather than calling getitem and setitem separately.

Source code in multicollections/__init__.py
110
111
112
113
114
115
116
117
118
119
120
121
122
@override
def setdefault(self, key: _K, default: _D | None = None, /) -> _V | _D | None:
    """Get the first value for a key, or set and return a default if not found.

    This is optimized to perform a single lookup in the key indices,
    rather than calling __getitem__ and __setitem__ separately.
    """
    if (indices := self._key_indices.get(key)) is not None:
        # Key exists, return its first value
        return self._items[indices[0]][1]
    # Key doesn't exist, add it with the default value
    self.add(key, default)
    return default

update(other: SupportsKeysAndGetItem[_K, _V] | Iterable[tuple[_K, _V]] = (), /, **kwargs: _V) -> None

Update the multi-mapping with items from another object.

This replaces existing values for keys found in the other object. This is optimized for batch operations.

Source code in multicollections/__init__.py
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
@override
def update(
    self,
    other: SupportsKeysAndGetItem[_K, _V] | Iterable[tuple[_K, _V]] = (),
    /,
    **kwargs: _V,
) -> None:
    """Update the multi-mapping with items from another object.

    This replaces existing values for keys found in the other object.
    This is optimized for batch operations.
    """
    # Collect all items first
    all_items = list(_yield_items(other, **kwargs))

    if not all_items:
        return

    # Get existing keys once for efficiency
    existing_keys = set(self._key_indices.keys())

    # Separate items into updates and additions
    updates_by_key, additions = self._collect_update_items(all_items, existing_keys)

    # Process updates efficiently
    if updates_by_key:
        self._process_updates(updates_by_key)

    # Add new items
    if additions:
        self._items.extend(additions)

    # Rebuild indices once at the end
    if updates_by_key or additions:
        self._rebuild_indices()

values() -> ValuesView[_V]

Return a view of the values in the MultiMapping.

Source code in multicollections/abc.py
230
231
232
233
@override
def values(self) -> ValuesView[_V]:
    """Return a view of the values in the MultiMapping."""
    return ValuesView(self)