mrkeyoor.com_
Sat 08 Aug 17:41 UTC
PyPIUtilsupdated 08 Aug 2026

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.

Verdict

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.

API stability5/5The project has existed since 2009 and its public model remains deliberately small: standard Mapping operations, inverse and inv views, explicit duplicate policies, frozenbidict, and OrderedBidict. The current package is still pre-1.0, but the README emphasizes long-term maintenance and stability, and the detailed semantics avoid ambiguous mutation behavior. Applications can pin a tiny surface and cover it thoroughly.
Docs5/5The versioned Read the Docs site explains why two synchronized mappings are necessary, constant-time inverse access, hashability, every key and value duplication case, atomic bulk insertion, immutable and ordered variants, order-sensitive equality, and extension points. Examples demonstrate the exact resulting mappings and exceptions rather than only showing happy paths, and the README directs readers to version-matched documentation.
Maintenance4/5The latest PyPI release, 0.23.1, was uploaded February 18, 2024, which is old enough to notice. However, the repository was pushed July 20, 2026, is not archived, and GitHub reports only two open issues and pull requests. The README reports full test coverage across supported Python versions plus property tests and benchmarks. The larger concern is bus factor: the maintainer says he has been the sole maintainer and active contributor for over 15 years.
Ecosystem4/5PyPIStats records 7,027,630 downloads in the latest week, the repository has 1,585 stars, and the README names organizations such as Google, Venmo, CERN, Baidu, and Tencent as users. The types implement familiar Mapping and MutableMapping contracts, so integration rarely needs adapters. Its scope is intentionally narrow and there is little plugin ecosystem, but that is an advantage for a foundational container.

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
Skip it if

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

PackageRegistryPick it when
two-way-dictPyPIA legacy project already depends on its minimal reverse-lookup API and changing behavior would cost more than the smaller feature set
multidictPyPIThe domain allows repeated keys or values, especially HTTP-style fields, and is not actually one-to-one
python-benedictPyPIBroad key-path, conversion, and file-format utilities matter more than a live type-safe inverse mapping