bidict: a bidirectional mapping that keeps the inverse view in sync
The bidirectional mapping library for Python.
At a glance
- What is it?
- bidict is a Python library for two-way mappings, where every key maps to a value and every value maps back to its key, with the inverse maintained as you mutate the original. It has no runtime dependencies outside the standard library, requires Python 3.11 or newer, and has been the project of one maintainer for around fifteen years.
- Who is it for?
- bidict fits a codebase that repeatedly needs the reverse lookup, a reverse index, or a two-way association kept consistent, and that wants a typed, dependency-free structure rather than two dictionaries maintained side by side. It is the wrong choice when a plain dict plus an occasional reverse comprehension would do, since a bidict carries real memory and update cost.
- Can I use it commercially?
- Yes, with conditions. MPL-2.0 is a weak copyleft licence: you can use it inside commercial and closed-source software, but if you distribute changes to its own files, you must publish those changes under the same licence.
- Is it still maintained?
- Yes. The repository last received commits 18 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 21, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One mapping, two views, and the invariant that holds them together
bidict is the bidirectional mapping library for Python, and the whole library can be explained in one example:
>>> from bidict import bidict
>>> element_by_symbol = bidict({'H': 'hydrogen'})
>>> element_by_symbol['H']
'hydrogen'
>>> element_by_symbol.inverse['hydrogen']
'H'The mapping behaves like a dict in the forward direction and exposes an `inverse` attribute that is itself a bidict, so the reverse lookup is a plain item access rather than a comprehension or a scan. That is the ergonomics.
The invariant underneath is the interesting part. The two directions are not two dictionaries that the library hopes you keep in sync, they are one structure with two views, and the library maintains the correspondence on every mutation. Insert a key and value and the inverse now maps that value back to the key. Delete a key and the inverse entry disappears with it.
That is what separates a bidict from the common workaround, which is two dicts and the discipline to update both. The discipline is exactly the thing that fails under concurrent access, in a refactor, or six months later when someone adds a new code path. A single structure with a maintained invariant has no way to drift.
The design has an obvious precondition. For the inverse to be well defined, the values must be unique. A bidict with two keys mapping to the same value has no single key to return from the inverse, so that is not a supported state, and the library's API is shaped around preventing it rather than around tolerating it.
The README is blunt about who this is for. It provides familiar, Pythonic APIs that are carefully designed for safety, simplicity, flexibility, and ergonomics, and it is implemented in concise, well-factored, fully type-hinted Python code.
No runtime dependencies, and that includes the type hints
One of the listed features is that bidict is lightweight, with no runtime dependencies outside Python's standard library, and the dependency list in pyproject.toml bears that out with a single conditional entry:
dependencies = [
# bidict/_typing.py's `typing`/`typing_extensions` imports branch on python<3.12
"typing_extensions>=4.0; python_version<'3.12'",
]The comment explains the whole thing. The only dependency is a backport of the typing module, and it is only installed on Python older than 3.12, because the internal typing module branches on the interpreter version. On a current interpreter, installing bidict adds nothing to your dependency tree at all.
That is a real operational property for a library that has been around since 2009 and is described as depended on by Google, Venmo, CERN, Baidu, Tencent, and teams across the world. A data structure that ends up in a large stack, directly or transitively, is one you do not want to bring a transitive dependency graph with. A dependency-free mapping is a thing you can add in a shared utility module without triggering a review conversation.
The Python floor is 3.11, and the classifiers name both CPython and PyPy, so it works on either implementation. The Typing:: Typed classifier says the package ships inline type information, and the source layout backs that up with a `bidict/_typing.py` module.
The development dependencies are more elaborate than the runtime ones, which is the normal shape for a library that treats its own test suite as part of the product. The test group includes coverage, hypothesis, pytest, pytest-benchmark, pytest-sphinx, pytest-xdist, sortedcollections, sortedcontainers, and the `ty` type checker. Hypothesis is the property-based testing library, which for a data structure is the right choice, since it can generate mapping operations and check the inverse invariant across a whole sequence of mutations rather than a handful of hand-written cases.
One maintainer for fifteen years, and the support structure around that
The maintenance story is stated more plainly than most libraries state theirs, and it is the single most important fact for an adopter.
The README says the author has been bidict's sole maintainer and active contributor since starting the project about fifteen years ago, and the help would be most welcome. The sponsoring section makes the same point in a different currency, describing bidict as the product of thousands of hours of unpaid work over the 15-plus years of sole maintainership.
The release history supports the continuity. The tags are v0.23.1 from 2024-02-18, v0.24.0 from 2026-08-25, and v0.24.1 from 2026-08-25, two releases on the same day, and the last push to the repository was on 2026-10-05. The current version in pyproject.toml is 0.24.2.dev0, so there is a development version in flight.
A version number still in the zero twenties after fifteen years is worth a moment of thought. It is a stable library with a stable API, but the author has kept it in the 0.x range, which conventionally signals that breaking changes are permitted. For a data structure that other people's code depends on, the practical effect is that you should pin a version and read the changelog when you upgrade, rather than assuming semver protection.
The support structure around the single maintainer is unusually complete. Enterprise-level support is offered through a Tidelift subscription or by contacting the author directly, and the author notes having a US-based LLC set up for invoicing and over 20 years of professional experience delivering software and support. Voluntary community support is through GitHub Discussions and the issue tracker, with the explicit instruction to search already-asked questions first. There is also a notice of usage asking users to star the repository, post in discussions, or send an email if their usage is significant in some way, which is a request rather than an obligation but says something about the project's funding model.
Sponsorship is offered through GitHub, PayPal, Tidelift, thanks.dev, Gumroad, or a support engagement with the author's LLC, and the project links to the Spotify FOSS fund announcement, a Sentry post about paying maintainers, and an Indeed engineering post as examples of companies funding open source in this way.
The test suite is the feature, and the tooling is unusually exact
The listed features include 100% test coverage running continuously across all supported Python versions, including property-based tests and benchmarks, and the repository layout shows what that means in practice.
The root of the tree is where the engineering tooling lives, and it is dense. There is `callgrind.py`, which is a driver for the Valgrind callgrind tool, so the C-level behaviour of the implementation is inspected rather than assumed. There is `microbenchmarks.py`. There is `compare_instructions.py`, which suggests an effort to compare the performance of alternative implementations. There is `check_dist`, a script that checks the built distribution. There is `init_dev_env` for bootstrapping a development environment, and `update_dev_dependencies`.
The configuration files are just as telling. `tox.ini` for the multi-environment test matrix, `codecov.yml` for coverage reporting, `.coveragerc` for coverage settings, `.pre-commit-config.yaml` for the pre-commit hooks, `_typos.toml` for a spell checker, `.readthedocs.yml` for the documentation build, `.envrc` and `flake.nix` with `flake.lock` for a Nix-based environment, and `uv.lock` for the development dependency lock.
The dependency groups are split by purpose, which tells you what the maintainer considers separate concerns. The test group is the coverage and property-testing stack. The dev group is pytest plugins for readable diffs and test output, plus tox and tox-uv. The docs group is Sphinx with the furo theme and a copybutton extension. The tool.uv section sets the default groups to test, dev, and docs, so a plain uv sync gets everything.
The build backend comment in pyproject.toml is the kind of detail worth reading slowly, because it explains a shipping bug. It says uv_build, not the uv on PATH, is what writes the wheel's RECORD, and it comes from a pin, and the floor is load-bearing: version 0.24.0 shipped hex-encoded RECORD hashes because the pin read a range that kept the wrong behaviour, and the fix tightened it. A single dependency pin that silently changed the format of the wheel's file manifest is exactly the kind of trap a lockfile floor is supposed to prevent, and the comment records the incident so it does not recur.
The library is a teaching artifact as much as a utility
There is a documentation section in the README called learning from bidict, and it makes a claim that is unusual for a data structure library.
The claim is that one of the best things about bidict is that it touches a surprising number of interesting Python corners, especially given its small size and scope. There is a dedicated document at the `learning-from-bidict` page for readers who are interested.
That framing explains several otherwise odd choices. A library whose job is to be a mapping with an inverse has no reason to use `__missing__`, `__getattr__` tricks, custom descriptors, or the more exotic corners of the data model. A library that is also a teaching artifact has a reason to use them, provided it does so where the use is clear.
It also explains the class layout. The `bidict/` package is not one module. A bidict is a mapping, so it needs the full `Mapping` protocol, and the natural way to get it without reimplementing iteration and containment is to inherit from a dict-like base. The package also holds a module for the typing helpers, `bidict/_typing.py`, which is where the version branching lives.
For a reader, the learning document is a reason to pick the library even if they do not end up using it. A small, well-tested, fully typed library that is also a tour of the parts of Python most codebases never touch is a better teaching artifact than a contrived one, because everything in it is load-bearing in production somewhere.
The documentation itself is hosted on readthedocs. The same documentation is published in several forms for different renderers, which is a deliberate choice for a project whose audience includes people reading on PyPI, on GitHub, and on a documentation site.
Where bidict is the wrong tool
The limits are specific and they are about the cost of the invariant, not about missing features.
First, values must be unique. This is the assumption the whole structure rests on, and code that violates it does not get a useful error, it gets ambiguous behaviour from the inverse. A mapping where several keys share a value, which is common in configuration and in reference tables, cannot be a bidict at all, and the answer there is a plain dict with a reverse index built on demand.
Second, memory and update cost are higher than a plain dict. A bidict holds the correspondence in both directions, so it stores more, and every mutation has to maintain the second direction. For a mapping that is written once and read many times, that is a poor trade, and for a mapping in a hot loop it can matter. The library's own `microbenchmarks.py` and `compare_instructions.py` exist to look at these numbers, which is the right place to find out rather than assuming.
Third, the two-dict workaround is often enough. If you need the reverse lookup in one specific place, a comprehension over the dict at that point is clearer than a bidict everywhere, and it does not constrain the values to be unique. Bidict earns its cost when reverse lookups are frequent and correctness of the correspondence matters over time.
Fourth, the 0.x version number means you should not assume semver protection, even though the API has been stable for fifteen years. Pin your version and read the changelog.
Fifth, and this is a bus factor point rather than a technical one, the project is one person's unpaid work. The stated plan is that help is welcome, and the project is looking for contributors, and the enterprise support route exists, but the reality is that one maintainer decides the roadmap. For a data structure this small, that has been fine for a long time, and it is a real consideration if it is on the critical path of something you cannot afford to wait on.
Sixth, the Python floor of 3.11 is recent enough to matter for some deployments, since a company on 3.9 or 3.10 cannot install the current release.
Editorial conclusion
bidict fits a codebase that repeatedly needs the reverse lookup, a reverse index, or a two-way association kept consistent, and that wants a typed, dependency-free structure rather than two dictionaries maintained side by side. It is the wrong choice when a plain dict plus an occasional reverse comprehension would do, since a bidict carries real memory and update cost. Check three things before you adopt it: that your values are unique, because that is the assumption the whole structure rests on, that your Python is 3.11 or newer, and that you read the documentation's guidance on when a bidict is not appropriate, since the library is explicit that it is not a drop-in replacement for every use of a dict.
Frequently asked questions
What is bidict and how do I use the inverse mapping?
bidict is a bidirectional mapping library for Python. You create a `bidict` from a mapping and access the reverse view through its `inverse` attribute, so `element_by_symbol['H']` gives the value and `element_by_symbol.inverse['hydrogen']` gives the key back. Install it with `pip install bidict`.
Does bidict have any runtime dependencies?
None outside Python's standard library. The only declared dependency is `typing_extensions>=4.0` and only on Python older than 3.12, because the internal typing module branches on the interpreter version. The package requires Python 3.11 or newer and is typed, with the Typing :: Typed classifier.
Who maintains bidict and how mature is it?
The author has been the sole maintainer and active contributor since starting the project about fifteen years ago, and the project is described as the product of thousands of hours of unpaid work. The library is used by organizations including Google, Venmo, CERN, Baidu, and Tencent, and enterprise support is available through Tidelift or by contacting the author directly.
How is bidict tested?
The project states 100% test coverage running continuously across all supported Python versions, including property-based tests and benchmarks. The test dependency group includes coverage, hypothesis, pytest, pytest-benchmark, pytest-sphinx, pytest-xdist, and the `ty` type checker, with tox.ini for the multi-environment matrix and codecov.yml for coverage reporting.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/jab-bidict)Was this useful?