How to Build a Two-Way Dictionary in Python
Implement a BiDict class that supports both forward key-to-value and reverse value-to-key lookups with a simple add, delete, and update API.
Python code
40 linesclass BiDict:
def __init__(self, data=None):
self.forward = {}
self.backward = {}
if data:
self.update(data)
def update(self, data):
for key, value in data.items():
self[key] = value
def __setitem__(self, key, value):
self.forward[key] = value
self.backward[value] = key
def __getitem__(self, key):
return self.forward[key]
def get_key(self, value):
return self.backward[value]
def __delitem__(self, key):
value = self.forward.pop(key)
del self.backward[value]
def __len__(self):
return len(self.forward)
def __repr__(self):
return f"BiDict({self.forward!r})"
if __name__ == "__main__":
bd = BiDict({"apple": "fruit", "carrot": "vegetable"})
print("Forward:", bd["apple"])
print("Backward:", bd.get_key("vegetable"))
bd["banana"] = "fruit"
print("After add:", bd)
del bd["carrot"]
print("After delete:", bd)
print("Size:", len(bd))
Output
Forward: fruit
Backward: vegetable
After add: BiDict({'apple': 'fruit', 'carrot': 'vegetable', 'banana': 'fruit'})
After delete: BiDict({'apple': 'fruit', 'banana': 'fruit'})
Size: 2
How it works
The BiDict class keeps two parallel dictionaries: forward maps keys to values and backward maps values back to keys. Every insertion via __setitem__ writes to both maps, keeping them in sync. Deleting a key removes the entry from both dicts using pop and the mirrored value. Because both lookups are plain dictionary accesses, forward and reverse lookups are O(1) on average. The class exposes standard dict-like methods (__getitem__, __delitem__, __len__) so it behaves naturally in loops and expressions.
Common mistakes
- Forgetting to enforce unique values — if two keys map to the same value, the backward map silently drops the first key.
- Mutating the `.forward` dictionary directly and leaving `.backward` out of sync.
- Not handling missing keys on reverse lookup with `.get()` or `KeyError`.
- Assuming values are hashable — unhashable values like lists will crash in the backward dict.
Variations
- Use the third-party `bidict` package for a production-ready, battle-tested implementation.
- Build a dataclass wrapper around two dicts and add type hints for compile-time safety.
Real-world use cases
- Mapping user IDs to usernames in a session manager so you can look up either direction fast.
- Translating between internal enum names and API response codes in a service layer.
- Matching city names to airport codes in a route-planning microservice without scanning lists.
Sponsored
More from Dictionaries & sets
- Build a Case-Insensitive Dict with a Wrapper Class in Python medium
- Build a defaultdict histogram of categories in Python easy
- Build adjacency dict graph from edges in Python easy
- Build an OrderedDict insertion order demo in Python 3 easy
- Check Invertible Mapping for Duplicate Values in Python easy
- Compare Two Dictionaries in Python easy
Keep learning
Related tutorials and quizzes for this topic.