bidict review
bidict is a Python mapping for one-to-one relationships. It indexes every pair in both directions, so `codes['GB']` can return a country name and `codes.inverse['United Kingdom']` can return the code without scanning the mapping. The inverse is a live view of the same pairs. The package also supplies `frozenbidict` for immutable, hashable mappings and `OrderedBidict` when forward and reverse iteration order must agree. Version 0.24.0 raises the Python floor to 3.11, removes `bidict.__version__` and other module metadata, repairs rollback during failed bulk writes, fixes several ordering cases, and deliberately stops reading pickles created by 0.23.1 or older.
Install bidict when the data is truly one-to-one and both lookup directions are part of normal application flow. Avoid it for one-to-many relationships, and treat the 0.24.0 Python floor and pickle break as real upgrade work.
We installed it
| Install | ✓ · 0.2s | 1 package on disk · 1 MB |
| Import | ✓ | import bidict in 0.09s · pure Python · py.typed · requires Python >=3.8 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does bidict install cleanly?
Yes. In a fresh container with an empty cache, pip install bidict finished in 0.2s, leaving 1 package and 1 MB on disk. pip-audit reported no known vulnerabilities.
What does bidict need to run?
Python >=3.8, and nothing compiled: it is pure Python. In our run import bidict succeeded in 0.09s, and the package ships py.typed for type checkers.
bidict or multidict: which should you use?
multidict: Choose it for one key with several values, especially repeated HTTP-style fields. Install bidict when the data is truly one-to-one and both lookup directions are part of normal application flow.
When should you not use bidict?
Several keys can point to the same value. bidict rejects that shape because the reverse lookup would be ambiguous; use a multimap or a dictionary of sets.
Use it if
- Each value is unique by domain rule, such as an external account ID paired with one internal account ID.
- Code reads the relationship in both directions often enough that rebuilding a reverse dictionary would be wasteful or error-prone.
- A duplicate value should raise at the write site instead of silently replacing an unrelated pair.
- You need the same bidirectional API in mutable, immutable, or order-aware forms.
- Several keys can point to the same value. bidict rejects that shape because the reverse lookup would be ambiguous; use a multimap or a dictionary of sets.
- Keys or values can be lists, dictionaries, or other unhashable objects. Both sides become indexes, so both sides must be hashable.
- The mapping is tiny, constructed once, and only reversed once. A checked dictionary comprehension is easier to explain and removes a dependency.
- You persist bidict instances with pickle across deployments. Version 0.24.0 cannot load pickles written by 0.23.1 or earlier, and the project does not promise pickle compatibility between releases.
- Your dependency policy disallows MPL 2.0 or requires several active maintainers. The repository names one maintainer and active contributor for the project's 15-plus-year history.
Setup reality
We installed bidict 0.23.1 in a clean Python 3.12 Bookworm container. The install finished in 0.2 seconds and left one package using 1 MB. It had no direct dependencies, pip-audit found no known vulnerabilities, and import bidict completed in 0.09 seconds. That release is pure Python, includes py.typed, uses MPL 2.0, and accepts Python 3.8 or newer. PyPI now serves 0.24.0, which requires Python 3.11 or newer, so older runtimes cannot take the current release.
The design constraint appears at the first write. Keys and values must be hashable, and values must be unique. Reusing a value raises ValueDuplicationError. Replacing an existing key with a new value follows dictionary behavior, while put() lets you choose an OnDup policy. forceput() and forceupdate() may discard conflicting pairs to restore the one-to-one invariant, so keep those calls visible in code review. Version 0.24.0 now rolls a bulk update back even when failure comes from unpacking, hashing, or a custom backing mapping rather than a duplication error.
inverse does not allocate a detached dictionary. It shares storage and mutability with the original object, which means a helper that receives mapping.inverse can change mapping. Use frozenbidict or pass a copy when that write access is unwanted. Plain bidict iteration now keeps keys() and values() paired correctly in 0.24.0, but use OrderedBidict when mutations must preserve the same order in both directions.
Two upgrade details deserve a deployment check. Read the installed version through importlib.metadata.version('bidict') because 0.24.0 removed bidict.__version__. Rebuild or migrate stored data instead of unpickling 0.23.1-era bidict objects under 0.24.0; the new release intentionally rejects those old pickles.
Patterns
Read a pair from either side lookup-both-directions
from bidict import bidict
country_by_code = bidict({'GB': 'United Kingdom', 'JP': 'Japan'})
print(country_by_code['GB'])
print(country_by_code.inverse['Japan'])The inverse lookup is backed by the same mapping and does not scan all pairs.
Add a pair through the inverse view write-through-inverse
country_by_code.inverse['Canada'] = 'CA'
assert country_by_code['CA'] == 'Canada'A mutable inverse writes into the original mapping. Treat it as shared write access, not a copy.
Reject an ambiguous reverse lookup catch-duplicate-value
from bidict import ValueDuplicationError, bidict
emails = bidict({1: 'ada@example.com'})
try:
emails[2] = 'ada@example.com'
except ValueDuplicationError:
print('email already assigned')The existing pair remains because one value cannot identify two keys.
Reject duplicate keys and values enforce-strict-write
from bidict import ON_DUP_RAISE, bidict
statuses = bidict({'ok': 200})
statuses.put('created', 201, on_dup=ON_DUP_RAISE)Normal item assignment may replace an existing key. ON_DUP_RAISE makes both sides strict.
Move a value to a new key replace-conflict-deliberately
from bidict import bidict
owner_by_id = bidict({'old-id': 'alice'})
owner_by_id.forceput('new-id', 'alice')
assert 'old-id' not in owner_by_idforceput removes conflicting associations. Use it only when replacement is the business rule.
Apply a strict batch bulk-write-atomically
from bidict import ON_DUP_RAISE, bidict
codes = bidict({'ok': 200})
codes.putall([('created', 201), ('accepted', 202)], on_dup=ON_DUP_RAISE)Version 0.24.0 rolls back the whole call when unpacking, hashing, backing-map writes, or duplicate handling raises.
Replace keys but protect values choose-duplicate-policy
from bidict import DROP_OLD, RAISE, OnDup, bidict
labels = bidict({1: 'one'})
policy = OnDup(key=DROP_OLD, val=RAISE)
labels.put(1, 'uno', on_dup=policy)Separate key and value actions make the intended loss explicit. Test a pair that conflicts on both sides.
Use a bidict as a cache key freeze-mapping
from bidict import frozenbidict
translation = frozenbidict({'yes': 'si', 'no': 'no'})
cache = {translation: 'spanish'}
assert cache[translation] == 'spanish'frozenbidict is immutable and hashable when all contained keys and values are hashable.
Keep both iteration orders aligned preserve-reverse-order
from bidict import OrderedBidict
steps = OrderedBidict([(1, 'draft'), (2, 'review')])
steps[2] = 'approved'
assert list(steps.values()) == list(steps.inverse.keys())Use OrderedBidict when mutation order is part of the contract on both views.
Compare mapping order explicitly compare-order
from bidict import OrderedBidict
a = OrderedBidict([(1, 'a'), (2, 'b')])
b = OrderedBidict([(2, 'b'), (1, 'a')])
assert a == b
assert not a.equals_order_sensitive(b)Normal mapping equality ignores order. equals_order_sensitive checks pair sequence too.
Swap pairs without a live bidict invert-pair-stream
from bidict import inverted
pairs = [('GB', 'United Kingdom'), ('JP', 'Japan')]
code_by_country = dict(inverted(pairs))The resulting dictionary is independent and will not stay synchronized with the original pairs.
Read package metadata on 0.24 read-installed-version
from importlib.metadata import version
print(version('bidict'))Version 0.24.0 removed bidict.__version__; use importlib.metadata for installed package metadata.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| multidict | PyPI | Choose it for one key with several values, especially repeated HTTP-style fields. |
| python-benedict | PyPI | Choose it when nested key paths, conversions, and file-format helpers matter more than a live inverse. |
| two-way-dict | PyPI | Keep it in an existing project already written around its smaller two-way mapping API. |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

