Other bidict Types

Now that we’ve covered basic usage of bidict.bidict and bidict.loosebidict, let’s look at the remaining bidict types and the hierarchy they belong to.

bidict Type Hierarchy

bidict type hierarchy

At the top of the hierarchy of types that bidict provides is bidict.BidirectionalMapping. This extends the collections.abc.Mapping ABC with the "inv" abstractproperty, as well as a concrete, generic implementation of __inverted__. BidirectionalMapping also implements __subclasshook__, so that any class providing a conforming API is considered a virtual subclass of BidirectionalMapping automatically.

Implementing BidirectionalMapping is BidictBase. Users will typically only interact with subclasses of this class.

Inheriting from BidictBase are the already-discussed bidict.bidict and bidict.loosebidict, which implement collections.abc.MutableMapping.

Also inheriting from BidictBase is bidict.frozenbidict, the first immutable bidict you’ll meet.

frozenbidict

Having bidict.BidirectionalMapping extend collections.abc.Mapping rather than MutableMapping allows for an immutable bidict type to extend from it. This type is called bidict.frozenbidict, and makes up the other branch of the tree.

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

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

Because it is immutable, frozenbidict implements collections.abc.Hashable. Thus it’s suitable for insertion into sets or other mappings:

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

See the bidict.frozenbidict and bidict.frozenorderedbidict API documentation for more information on implementation details if necessary.

orderedbidict

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 and friends have 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
'hydrogen'
>>> second
'helium'
>>> third
'lithium'
>>> element_by_symbol.inv['beryllium'] = 'Be'
>>> last = next(reversed(element_by_symbol))
>>> last
'Be'

The additional methods of collections.OrderedDict are supported too:

>>> element_by_symbol.popitem(last=True)
('Be', 'beryllium')
>>> element_by_symbol.popitem(last=False)
('H', 'hydrogen')
>>> element_by_symbol['H'] = 'hydrogen'
>>> element_by_symbol
orderedbidict([('He', 'helium'), ('Li', 'lithium'), ('H', 'hydrogen')])
>>> element_by_symbol.move_to_end('Li')  # works on Python < 3.2 too
>>> element_by_symbol
orderedbidict([('He', 'helium'), ('H', 'hydrogen'), ('Li', 'lithium')])
>>> element_by_symbol.move_to_end('H', last=False)
>>> element_by_symbol
orderedbidict([('H', 'hydrogen'), ('He', 'helium'), ('Li', 'lithium')])

As with OrderedDict, updating an existing item preserves its position in the order, while deleting an item and re-adding it moves it to the end:

>>> element_by_symbol['He'] = 'HELIUM'
>>> element_by_symbol
orderedbidict([('H', 'hydrogen'), ('He', 'HELIUM'), ('Li', 'lithium')])
>>> del element_by_symbol['H']
>>> element_by_symbol['H'] = 'hydrogen'
>>> element_by_symbol
orderedbidict([('He', 'HELIUM'), ('Li', 'lithium'), ('H', 'hydrogen')])

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)
>>> o
orderedbidict([(1, 2), (3, 8), (5, 6)])
>>> o = orderedbidict([(1, 2), (3, 4), (5, 6), (7, 8)])
>>> o.forceput(5, 2)
>>> o
orderedbidict([(3, 4), (5, 2), (7, 8)])

Equality tests between two ordered bidicts are order-sensitive. Equality tests between an ordered bidict and an instance of some other Mapping type (including even OrderedDict) are order-insensitive.

>>> o1 = orderedbidict([('one', 1), ('two', 2)])
>>> o2 = orderedbidict([('two', 2), ('one', 1)])
>>> o1 == o2
False
>>> o1 == dict(o2)
True
>>> from collections import OrderedDict
>>> o1 == OrderedDict(o2)
True

To customize this behavior, you can override _should_compare_order_sensitive in a subclass:

>>> class MyOrderedBidict(orderedbidict):
...    def _should_compare_order_sensitive(self, mapping):
...        # Can return False to never do order-sensitive comparison. Or
...        # can return isinstance(mapping, (orderedbidict, OrderedDict)).
...        # To make order-sensitive only if mapping is reversible:
...        return bool(getattr(mapping, '__reversed__', None))

>>> m = MyOrderedBidict(o1)
>>> m == OrderedDict(o2)  # OrderedDict is reversible -> order-sensitive
False
>>> m == dict(o2) if CPYTHON else m != dict(o2)  
True

Note that with the custom _should_compare_order_sensitive implementation above, comparing with a dict would be order-sensitive in PyPy but not in CPython, because on PyPy dicts implement __reversed__ (!), whereas on CPython they do not. Watch out for that if you ever want to do something like this, and expect consistency between CPython and PyPy.

orderedbidict also comes in loose and frozen flavors.

namedbidict

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

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

The base_type keyword arg, whose default value is bidict.bidict, allows overriding the bidict type used as the base class, allowing the creation of e.g. namedfrozenbidicts:

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

Polymorphism

Note that none of the bidict types inherit from dict:

>>> from bidict import bidict
>>> isinstance(bidict(), dict)
False

If you must use isinstance() to check whether a bidict is dict-like, you can use the abstract base classes from the collections module, which is a better way to check for interface conformance:

>>> from collections import Mapping, MutableMapping
>>> isinstance(bidict(), Mapping)
True
>>> isinstance(bidict(), MutableMapping)
True

Though you can often write more polymorphic code by using duck typing rather than isinstance():

>>> # LBYL-style:
>>> if hasattr(foo, '__setitem__'):  
...     foo['bar'] = 'baz'
>>> # EAFP-style:
>>> try:  
...     foo['bar'] = 'baz'
... except TypeError:
...     # plan B

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. 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 _fwd_class and _inv_class 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 sortedbidict1(bidict.bidict):
...     _fwd_class = sortedcontainers.SortedDict
...     _inv_class = sortedcontainers.SortedDict
...     __reversed__ = lambda self: reversed(self._fwd)

>>> b = sortedbidict1({'Tokyo': 'Japan', 'Cairo': 'Egypt'})
>>> b
sortedbidict1([('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 sortedbidict2(bidict.bidict):
...     _fwd_class = sortedcontainers.SortedDict
...     _inv_class = sortedcollections.ValueSortedDict
...     __reversed__ = lambda self: reversed(self._fwd)

>>> elemByAtomicNum = sortedbidict2({3: 'lithium', 1: 'hydrogen', 2: 'helium'})

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

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

>>> elemByAtomicNum[4] = 'beryllium'

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

>>> # order is preserved correctly when passing .inv back into constructor:
>>> atomicNumByElem = sortedbidict2(elemByAtomicNum.inv)
>>> list(atomicNumByElem.items()) == list(elemByAtomicNum.inv.items())
True
>>> # copies of .inv preserve order correctly too:
>>> list(elemByAtomicNum.inv.copy().items()) == list(atomicNumByElem.items())
True

>>> # To pass method calls through to the _fwd 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._fwd, name)
...         except AttributeError:
...             raise e

>>> sortedbidict2.__getattribute__ = __getattribute__

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