Other bidict Types

Now that we’ve covered Basic Usage, let’s look at the remaining bidict types.

bidict Types Diagram

bidict types diagram

The most abstract type that bidict provides is bidict.BidirectionalMapping. This extends the collections.abc.Mapping ABC by adding the “invabstractproperty. It also implements __subclasshook__(), so that any class providing a conforming API is considered a virtual subclass of BidirectionalMapping automatically, without needing to subclass BidirectionalMapping explicitly.


The simplest bidict type that implements BidirectionalMapping is frozenbidict, which provides an immutable, hashable bidirectional mapping.

As you would expect, attempting to mutate a frozenbidict causes an error:

>>> from bidict import frozenbidict
>>> f = frozenbidict({'H': 'hydrogen'})
>>> f['C'] = 'carbon'
Traceback (most recent call last):
TypeError: ...

frozenbidict also implements collections.abc.Hashable, so it’s suitable for insertion into sets or other mappings:

>>> {f} is not 'an error'
>>> {f: 1} is not 'an error'

See the frozenbidict API documentation for more information.


For those times when your one-to-one mapping must also support remembering the order in which items were inserted (à la collections.OrderedDict), bidict.OrderedBidict has got your back:

>>> from bidict import OrderedBidict
>>> element_by_symbol = OrderedBidict([
...     ('H', 'hydrogen'), ('He', 'helium'), ('Li', 'lithium')])

>>> element_by_symbol.inv
OrderedBidict([('hydrogen', 'H'), ('helium', 'He'), ('lithium', 'Li')])

>>> first, second, third = element_by_symbol.values()
>>> first, second, third
('hydrogen', 'helium', 'lithium')

>>> # Insert an additional item and verify it now comes last:
>>> element_by_symbol['Be'] = 'beryllium'
>>> last_item = list(element_by_symbol.items())[-1]
>>> last_item
('Be', 'beryllium')

The additional methods of OrderedDict are supported too:

>>> element_by_symbol.popitem(last=True)  # Remove the last inserted item
('Be', 'beryllium')
>>> element_by_symbol.popitem(last=False) # Remove the first inserted item
('H', 'hydrogen')

>>> # Re-adding hydrogen after it's been removed moves it to the last item:
>>> element_by_symbol['H'] = 'hydrogen'
>>> element_by_symbol
OrderedBidict([('He', 'helium'), ('Li', 'lithium'), ('H', 'hydrogen')])

>>> # But there's also a `move_to_end` method just for this purpose:
>>> element_by_symbol.move_to_end('Li')
>>> element_by_symbol
OrderedBidict([('He', 'helium'), ('H', 'hydrogen'), ('Li', 'lithium')])

>>> element_by_symbol.move_to_end('H', last=False)  # move to front
>>> element_by_symbol
OrderedBidict([('H', 'hydrogen'), ('He', 'helium'), ('Li', 'lithium')])

As with OrderedDict, updating an existing item preserves its position in the order:

>>> element_by_symbol['He'] = 'updated in place!'
>>> element_by_symbol
OrderedBidict([('H', 'hydrogen'), ('He', 'updated in place!'), ('Li', 'lithium')])

When setting an item whose key duplicates that of an existing item and whose value duplicates that of a different existing item, the existing item whose value is duplicated will be dropped, and the existing item whose key is duplicated will have its value overwritten in place:

>>> o = OrderedBidict([(1, 2), (3, 4), (5, 6), (7, 8)])
>>> o.forceput(3, 8)  # item with duplicated value (7, 8) is dropped...
>>> o  # and the item with duplicated key (3, 4) is updated in place:
OrderedBidict([(1, 2), (3, 8), (5, 6)])
>>> # (3, 8) took the place of (3, 4), not (7, 8)

>>> o = OrderedBidict([(1, 2), (3, 4), (5, 6), (7, 8)])  # as before
>>> o.forceput(5, 2)  # another example
>>> o
OrderedBidict([(3, 4), (5, 2), (7, 8)])
>>> # (5, 2) took the place of (5, 6), not (1, 2)

__eq__() is order-insensitive

To ensure that equality of bidicts is transitive (enabling conformance to the Liskov substitution principle), equality tests between an ordered bidict and other Mappings are always order-insensitive:

>>> from bidict import bidict
>>> b = bidict([('one', 1), ('two', 2)])
>>> o1 = OrderedBidict([('one', 1), ('two', 2)])
>>> o2 = OrderedBidict([('two', 2), ('one', 1)])
>>> b == o1
>>> b == o2
>>> o1 == o2

For order-sensitive equality tests, use equals_order_sensitive():

>>> o1.equals_order_sensitive(o2)
>>> from collections import OrderedDict
>>> od = OrderedDict(o2)
>>> o1.equals_order_sensitive(od)

Note that this differs from the behavior of collections.OrderedDict’s __eq__(), by recommendation of Raymond Hettinger (the author) himself (who said that making OrderedDict’s __eq__() intransitive was a mistake).

OrderedBidict also comes in a frozen flavor. See the FrozenOrderedBidict API documentation for more information.


bidict.namedbidict(), inspired by collections.namedtuple(), allows you to easily generate a new bidirectional mapping type with custom attribute-based access to forward and inverse mappings:

>>> from bidict import namedbidict
>>> ElementMap = namedbidict('ElementMap', 'symbol', 'name')
>>> noble_gases = ElementMap(He='helium')
>>> noble_gases.name_for['He']
>>> noble_gases.symbol_for['helium']
>>> noble_gases.name_for['Ne'] = 'neon'
>>> del noble_gases.symbol_for['helium']
>>> noble_gases
ElementMap({'Ne': 'neon'})

Using the base_type keyword arg – whose default value is bidict.bidict – you can override the bidict type used as the base class, allowing the creation of e.g. a named frozenbidict type:

>>> from bidict import frozenbidict
>>> ElMap = namedbidict('ElMap', 'symbol', 'name', base_type=frozenbidict)
>>> noble = ElMap(He='helium')
>>> noble.symbol_for['helium']
>>> hash(noble) is not 'an error'
>>> noble['C'] = 'carbon'  # mutation fails
Traceback (most recent call last):
TypeError: ...


a.k.a “Know your ABCs”

You may be tempted to write something like isinstance(obj, dict) to check whether obj is a Mapping. However, this check is too specific, and will fail for many types that implement the Mapping interface:

>>> from collections import ChainMap  # (Python 3+) 
>>> issubclass(ChainMap, dict)  

The same is true for all the bidict types:

>>> from bidict import bidict
>>> issubclass(bidict, dict)

The proper way to check whether an object is a Mapping is to use the abstract base classes (ABCs) from the collections module that are provided for this purpose:

>>> from collections import Mapping  
>>> issubclass(ChainMap, Mapping)  
>>> isinstance(bidict(), Mapping)  

Also note that the proper way to check whether an object is an (im)mutable mapping is to use the MutableMapping ABC:

>>> from collections import MutableMapping
>>> from bidict import BidirectionalMapping, frozenbidict

>>> def is_immutable_bimap(obj):
...     return (isinstance(obj, BidirectionalMapping)
...             and not isinstance(obj, MutableMapping))

>>> is_immutable_bimap(bidict())

>>> is_immutable_bimap(frozenbidict())

Checking for isinstance(obj, frozenbidict) is too specific and could fail in some cases. For example, FrozenOrderedBidict is an immutable mapping but it does not subclass frozenbidict:

>>> from bidict import FrozenOrderedBidict
>>> obj = FrozenOrderedBidict()
>>> is_immutable_bimap(obj)
>>> isinstance(obj, frozenbidict)

Besides the above, there are several other collections ABCs whose interfaces are implemented by various bidict types. Have a look through the collections.abc documentation if you’re interested.

One thing you might notice is that there is no Ordered or OrderedMapping ABC. However, Python 3.6 introduced the collections.abc.Reversible ABC. Since being reversible implies having an ordering, you could check for reversibility instead. For example:

>>> from collections import Reversible  
>>> def is_reversible_mapping(cls):
...     return issubclass(cls, Reversible) and issubclass(cls, Mapping)

>>> from bidict import OrderedBidict
>>> is_reversible_mapping(OrderedBidict)  

>>> from collections import OrderedDict
>>> is_ordered_mapping(OrderedDict)  

Extending bidict

Although bidict ships with all the bidict types we just covered, it’s always possible users might need something more than what’s provided. For this reason, bidict was written with extensibility in mind.

Let’s look at some examples.

OverwritingBidict Recipe

If you’d like OVERWRITE to be the default duplication policy for __setitem__() and update(), rather than always having to use forceput() and forceupdate(), you can use the following recipe:

>>> from bidict import bidict, OVERWRITE

>>> class OverwritingBidict(bidict):
...     on_dup_val = OVERWRITE

>>> b = OverwritingBidict({'one': 1})
>>> b['two'] = 1  # succeeds, no ValueDuplicationError
>>> b
OverwritingBidict({'two': 1})

>>> b.update({'three': 1})  # ditto
>>> b
OverwritingBidict({'three': 1})

As with bidict.bidict, OverwritingBidict.put() and OverwritingBidict.putall() will still provide per-call overrides for duplication policies, and will both still default to raising for all duplication types unless you override those methods too.

To make an overwriting ordered bidict, simply adapt this recipe to have the class inherit from bidict.OrderedBidict.


With a default OVERWRITE policy as in the OverwritingBidict recipe above, beware of the following potentially surprising behavior:

>>> b = OverwritingBidict({'one': 1, 'two': 2})
>>> b['one'] = 2
>>> b
OverwritingBidict({'one': 2})

That is, setting an existing key to the value of a different existing item causes both existing items to be collapsed into a single item.

Sorted Bidict Recipes

Suppose you need a bidict that maintains its items in sorted order. The Python standard library does not include any sorted dict types, but the excellent sortedcontainers and sortedcollections libraries do. Armed with these along with bidict’s _fwdm_cls and _invm_cls attributes, creating a sorted bidict type is dead simple:

>>> import bidict, sortedcontainers

>>> # a sorted bidict whose forward items stay sorted by their keys,
>>> # and whose inverse items stay sorted by *their* keys (i.e. it and
>>> # its inverse iterate over their items in different orders):

>>> class SortedBidict(bidict.bidict):
...     _fwdm_cls = sortedcontainers.SortedDict
...     _invm_cls = sortedcontainers.SortedDict
...     # Include this for nicer repr's:
...     __repr_delegate__ = lambda x: list(x.items())

>>> b = SortedBidict({'Tokyo': 'Japan', 'Cairo': 'Egypt'})
>>> b
SortedBidict([('Cairo', 'Egypt'), ('Tokyo', 'Japan')])

>>> b['Lima'] = 'Peru'

>>> # b stays sorted by its keys:
>>> list(b.items())
[('Cairo', 'Egypt'), ('Lima', 'Peru'), ('Tokyo', 'Japan')]

>>> # b.inv stays sorted by *its* keys (b's values!)
>>> list(b.inv.items())
[('Egypt', 'Cairo'), ('Japan', 'Tokyo'), ('Peru', 'Lima')]

>>> # a sorted bidict whose forward items stay sorted by their keys,
>>> # and whose inverse items stay sorted by their values (i.e. it and
>>> # its inverse iterate over their items in the same order):

>>> import sortedcollections

>>> class KeySortedBidict(bidict.bidict):
...     _fwdm_cls = sortedcontainers.SortedDict
...     _invm_cls = sortedcollections.ValueSortedDict
...     # Include this for nicer repr's:
...     __repr_delegate__ = lambda x: list(x.items())

>>> element_by_atomic_number = KeySortedBidict({
...     3: 'lithium', 1: 'hydrogen', 2: 'helium'})

>>> # stays sorted by key:
>>> element_by_atomic_number
KeySortedBidict([(1, 'hydrogen'), (2, 'helium'), (3, 'lithium')])

>>> # .inv stays sorted by value:
>>> list(element_by_atomic_number.inv.items())
[('hydrogen', 1), ('helium', 2), ('lithium', 3)]

>>> element_by_atomic_number[4] = 'beryllium'

>>> list(element_by_atomic_number.inv.items())
[('hydrogen', 1), ('helium', 2), ('lithium', 3), ('beryllium', 4)]

>>> # This works because a bidict whose _fwdm_cls differs from its _invm_cls computes
>>> # its inverse class -- which (note) is not actually the same class as the original,
>>> # as it needs to have its _fwdm_cls and _invm_cls swapped -- automatically.
>>> # You can see this if you inspect the inverse bidict:
>>> element_by_atomic_number.inv  # Note the different class, which was auto-generated:
KeySortedBidictInv([('hydrogen', 1), ('helium', 2), ('lithium', 3), ('beryllium', 4)])
>>> ValueSortedBidict = element_by_atomic_number.inv.__class__
>>> ValueSortedBidict._fwdm_cls
<class 'sortedcollections.recipes.ValueSortedDict'>
>>> ValueSortedBidict._invm_cls
<class 'sortedcontainers.sorteddict.SortedDict'>

>>> # Round trips work as expected:
>>> atomic_number_by_element = ValueSortedBidict(element_by_atomic_number.inv)
>>> atomic_number_by_element
KeySortedBidictInv([('hydrogen', 1), ('helium', 2), ('lithium', 3), ('beryllium', 4)])
>>> KeySortedBidict(atomic_number_by_element.inv) == element_by_atomic_number

>>> # One other useful trick:
>>> # To pass method calls through to the _fwdm SortedDict when not present
>>> # on the bidict instance, provide a custom __getattribute__ method:
>>> def __getattribute__(self, name):
...     try:
...         return object.__getattribute__(self, name)
...     except AttributeError as e:
...         try:
...             return getattr(self._fwdm, name)
...         except AttributeError:
...             raise e

>>> KeySortedBidict.__getattribute__ = __getattribute__

>>> # bidict has no .peekitem attr, so the call is passed through to _fwdm:
>>> element_by_atomic_number.peekitem()
(4, 'beryllium')
>>> element_by_atomic_number.inv.peekitem()
('beryllium', 4)

Next proceed to Other Functionality.