Other bidict Types

Now that we’ve covered basic usage, 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 frozenbidict, which provides a hashable, immutable bidict type, and serves as a base class for mutable bidict types to extend.

frozenbidict

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...

frozenbidict also implements collections.abc.Hashable, so 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 frozenbidict API documentation for more information.

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 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
'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)])

To ensure that equality of bidicts is transitive, and to comply with 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
True
>>> b == o2
True
>>> o1 == o2
True

For order-sensitive equality tests, use equals_order_sensitive:

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

Note that this differs from the behavior of collections.OrderedDict.__eq__(), by recommendation of Raymond Hettinger himself.

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

namedbidict

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', '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. named frozen bidicts:

>>> 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 the proper way to check if an object is a mapping:

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

Of course you can also use duck typing to avoid isinstance() altogether:

>>> # EAFP-style:
>>> try:  
...     foo['bar'] = 'baz'
... except TypeError:
...     # plan B

>>> # LBYL-style:
>>> if hasattr(foo, '__setitem__'):  
...     foo['bar'] = 'baz'

Also note that since bidict extends frozenbidict, if you need to check whether a bidict is immutable, testing for isinstance(foo, frozenbidict) is not what you want:

>>> from bidict import frozenbidict
>>> isinstance(bidict(), frozenbidict)
True

Instead you can check for isinstance(foo, Hashable) or isinstance(foo, MutableMapping) to get the desired behavior:

>>> from collections import Hashable
>>> isinstance(frozenbidict(), Hashable)
True
>>> isinstance(bidict(), Hashable)
False
>>> isinstance(bidict(), MutableMapping)
True
>>> isinstance(frozenbidict(), MutableMapping)
False

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.

Beware of OVERWRITE

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 fwd_cls and inv_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 SortedBidict1(bidict.bidict):
...     fwd_cls = sortedcontainers.SortedDict
...     inv_cls = sortedcontainers.SortedDict
...     __reversed__ = lambda self: reversed(self.fwdm)

>>> 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_cls = sortedcontainers.SortedDict
...     inv_cls = sortedcollections.ValueSortedDict
...     __reversed__ = lambda self: reversed(self.fwdm)

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

>>> # stays sorted by key:
>>> element_by_atomic_number
SortedBidict2([(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)]

>>> # order is preserved correctly when passing .inv back into constructor:
>>> atomic_number_by_element = SortedBidict2(element_by_atomic_number.inv)
>>> list(atomic_number_by_element.items()) == list(element_by_atomic_number.inv.items())
True
>>> # copies of .inv preserve order correctly too:
>>> list(element_by_atomic_number.inv.copy().items()) == list(atomic_number_by_element.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.fwdm, name)
...         except AttributeError:
...             raise e

>>> SortedBidict2.__getattribute__ = __getattribute__

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

Proceed to Other Functionality.