bidict
bidict provides Python mappings that maintain a one-to-one relationship in both directions. A normal lookup returns the value for a key, while the constant-time inverse view returns the key for a value; mutations through either view keep both sides synchronized. The package includes mutable bidict, immutable and hashable frozenbidict, and OrderedBidict for order-sensitive operations. It has no runtime dependencies, includes type hints, and gives explicit policies for duplicate keys and values instead of silently losing an existing association.
The right small dependency for a genuine mutable bijection; its duplicate rules are safer than maintaining two dictionaries by hand. Do not force one-to-many data into it, and review MPL 2.0 if your organization treats licenses conservatively.
Use it if
- Your domain contains a real bijection such as internal IDs to unique external IDs, symbols to canonical names, or enum values to unique labels
- Both forward and reverse lookups must be constant-time and mutations must keep the two directions consistent
- Duplicate values are data errors that should raise unless the caller explicitly chooses a replacement policy
- You need an immutable hashable mapping or an ordered bidirectional mapping in addition to the common mutable form
- Multiple keys may legitimately share one value: bidict enforces unique values and raises ValueDuplicationError, so a dict plus a defaultdict or a multimap models one-to-many data correctly
- Values include lists, dictionaries, or other unhashable objects: reverse lookup requires values to be hashable just like ordinary dict keys
- You only reverse a small mapping once and never mutate it: a checked dict comprehension is simpler and avoids maintaining two internal mappings
- You assume plain bidict preserves identical forward and inverse insertion order after updates: the docs show those orders can diverge and require OrderedBidict, which carries a higher constant space cost
- Your licensing policy rejects file-level copyleft or single-maintainer dependencies: bidict uses MPL 2.0 and its README says the project has had one maintainer and active contributor for more than 15 years
Setup reality
pip install bidict is all the package setup: version 0.23.1 supports Python 3.8 and newer, has no runtime dependencies, and includes py.typed metadata. The real setup is deciding whether the data is truly one-to-one. Both keys and values must be hashable, and every value must be unique. Assigning an existing key to a new unused value behaves like dict and replaces that key's old association. Assigning a new key to an already-used value raises ValueDuplicationError to prevent silent loss. forceput and forceupdate deliberately remove conflicting associations; put and putall accept OnDup policies for stricter or custom behavior. Bulk putall is transactional in the important sense documented by the implementation: if an item raises, none of that call's items are inserted. The inverse property is a live mapping backed by the same data, not a copy, and can be mutated directly. That is convenient but means passing inverse to another function grants mutation of the original unless you use frozenbidict or a copy. Plain bidict uses two dictionary-like indexes and therefore O(n) space with a larger constant than dict. Python dict insertion order does not guarantee that forward and inverse views stay in matching order after changing one side; use OrderedBidict when that invariant matters and equals_order_sensitive when equality must include order. OrderedBidict costs another constant-factor increase. The package is MPL 2.0 rather than MIT or BSD, so organizations with restrictive dependency policies should review the license. Release 0.23.1 dates to February 2024, though repository work continued in July 2026; pinning is easy because the API is small and mature.
Patterns
Create forward and inverse lookupscreate-bidirectional-map
from bidict import bidict
element_by_symbol = bidict({
'H': 'hydrogen',
'He': 'helium',
})
print(element_by_symbol['H'])
print(element_by_symbol.inverse['helium'])inverse is a constant-time live view, not a newly computed dictionary. Values must be unique and hashable.
Update the original through its inversemutate-through-inverse
element_by_symbol.inverse['lithium'] = 'Li'
del element_by_symbol.inverse['hydrogen']
assert element_by_symbol == {'He': 'helium', 'Li': 'lithium'}The inverse is mutable when the original is mutable. Passing it to other code grants write access to the same associations.
Handle a conflicting value explicitlyreject-duplicate-value
from bidict import ValueDuplicationError
users = bidict({'u1': 'ada@example.com'})
try:
users['u2'] = 'ada@example.com'
except ValueDuplicationError as error:
report_duplicate_email(error.args[0])A new key cannot reuse an existing value because reverse lookup would become ambiguous. The original mapping remains intact.
Force a new associationreplace-conflicting-pair
users = bidict({'u1': 'ada@example.com'})
users.forceput('u2', 'ada@example.com')
assert 'u1' not in users
assert users['u2'] == 'ada@example.com'forceput removes every conflicting association needed to preserve uniqueness. Use it only when discarding the previous pair is intended.
Use strict insertion for both sidesrequire-no-duplicates
from bidict import ON_DUP_RAISE
codes = bidict({'ok': 200})
codes.put('created', 201, on_dup=ON_DUP_RAISE)Normal item assignment permits replacing an existing key with a fresh value. put with ON_DUP_RAISE rejects duplicate keys as well as duplicate values.
Insert a batch without partial updatesbulk-insert-atomically
from bidict import ON_DUP_RAISE, ValueDuplicationError
codes = bidict({'ok': 200})
try:
codes.putall([('created', 201), ('success', 200)], on_dup=ON_DUP_RAISE)
except ValueDuplicationError:
pass
assert codes == {'ok': 200}putall documents all-or-nothing behavior for an insertion error; none of the batch is retained when one item conflicts.
Choose separate key and value policiescustomize-duplicate-policy
from bidict import OnDup, DROP_OLD, RAISE
on_dup = OnDup(key=DROP_OLD, val=RAISE)
labels = bidict({1: 'one'})
labels.put(1, 'uno', on_dup=on_dup)When an item conflicts with one existing key and another existing value, the value policy takes precedence. Test that three-item case explicitly.
Use a hashable frozen mappingcreate-immutable-bidict
from bidict import frozenbidict
headers = frozenbidict({'content-type': 'Content-Type'})
cache = {headers: 'normalized-header-map'}
print(headers.inverse['Content-Type'])frozenbidict is immutable and hashable, so it can be a set member or dictionary key when all contained keys and values are hashable.
Keep forward and inverse insertion order alignedpreserve-both-orders
from bidict import OrderedBidict
items = OrderedBidict([(1, 'one'), (2, 'two'), (3, 'three')])
items[2] = 'TWO'
assert list(items.values()) == ['one', 'TWO', 'three']
assert list(items.inverse.keys()) == ['one', 'TWO', 'three']Plain bidict order can diverge between forward and inverse after replacing a value. OrderedBidict preserves the stronger ordering invariant at extra memory cost.
Include order in an equality checkcompare-order-sensitively
from bidict import OrderedBidict
left = OrderedBidict([(1, 'one'), (2, 'two')])
right = OrderedBidict([(2, 'two'), (1, 'one')])
assert left == right
assert not left.equals_order_sensitive(right)Regular mapping equality is intentionally order-insensitive, including for OrderedBidict. Use equals_order_sensitive when sequence is part of the contract.
Swap an iterable of pairsinvert-pairs-lazily
from bidict import inverted
pairs = [('H', 'hydrogen'), ('He', 'helium')]
by_name = dict(inverted(pairs))
assert by_name['hydrogen'] == 'H'inverted swaps pairs from any compatible iterable. A plain dict built from the result does not keep a synchronized inverse after later mutations.
Carry key and value types through the inversetype-forward-and-reverse
from bidict import bidict
user_by_id: bidict[int, str] = bidict({1: 'ada', 2: 'grace'})
name: str = user_by_id[1]
user_id: int = user_by_id.inverse['grace']The package ships py.typed metadata. Static typing cannot prove runtime uniqueness, so duplicate-value exceptions still need deliberate handling.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| two-way-dict | PyPI | A legacy project already depends on its minimal reverse-lookup API and changing behavior would cost more than the smaller feature set |
| multidict | PyPI | The domain allows repeated keys or values, especially HTTP-style fields, and is not actually one-to-one |
| python-benedict | PyPI | Broad key-path, conversion, and file-format utilities matter more than a live type-safe inverse mapping |