Quacking like a mapping in Python PREMIUM

What's required for an object to be considered a mapping in Python, and what does Python's duck typing mean for mapping objects?

Trey Hunner Trey Hunner 4 min read 04:20 video Python 3.10—3.14
Watch this as a 04:20 screencast
Same content, narrated.
Watch it

What's required to make an object a mapping?

Well, it depends on what you mean.

From a user's perspective

A mapping is a dictionary-like object.

As a Python user, if you tell me that an object is a mapping, I'll expect that it acts like a dictionary.

In particular, I'd hope to see a keys method:

>>> counts = {"purple": 3, "blue": 2, "green": 1}
>>> counts.keys()
dict_keys(['purple', 'blue', 'green'])

And a values method:

>>> counts.values()
dict_values([3, 2, 1])

And an items method:

>>> counts.items()
dict_items([('purple', 3), ('blue', 2), ('green', 1)])

And iterability. In particular, looping over it should give keys:

>>> for key in counts:
...     print(key)
...
purple
blue
green

Also, it should work with the built-in len function:

>>> len(counts)
3

And it should support containment checks. Using the in operator should tell us whether a particular key is in the dictionary:

>>> "purple" in counts
True

Also, it needs to support subscripting to get the value for a key:

>>> counts["purple"]
3

I'd also probably expect to see various methods that dictionaries tend to have, like the get method, and the fromkeys class method.

If it's a mutable mapping, I'd also want to see an update method, and a setdefault method, and a pop method, and so on.

From Python's perspective

But from Python's perspective, what's required for a minimum viable mapping object?

Of course, that also depends on what we mean.

The one mapping-specific syntax that Python currently supports is the double star (**) syntax. Double star is how we unpack a mapping into keyword arguments in a function call, or into a new dictionary.

What does this syntax require?

It turns out it only requires two things: a keys method, and the ability to look up keys with subscripting.

From the perspective of that double star syntax, this class is actually a valid mapping class:

>>> class MyMapping:
...     def keys(self):
...         return ["a", "b", "c"]
...     def __getitem__(self, key):
...         return 4
...

When we make a new MyMapping object here, we can use ** to unpack it into a new dictionary:

>>> {**MyMapping()}
{'a': 4, 'b': 4, 'c': 4}

That's kind of weird. There's no get method, there's no containment checking, there's no iterability.

As a Python user, I would not call instances of this class valid mappings. But remember that when Python uses an object, it doesn't necessarily need a fully valid version of that object. It just needs enough functionality to perform the task at hand.

Quacking like a mapping

Many Python features rely on duck typing.

Tuple unpacking works on any iterable. We've just unpacked a string into two variables:

>>> first, *rest = "Python"
>>> first
'P'
>>> rest
['y', 't', 'h', 'o', 'n']

first is the first character in the string, and rest is the rest of the characters in the string.

Python's print function accepts a file argument, which can be any object that has a write method:

>>> class FakeFile:
...     def __init__(self):
...         self.text = ""
...     def write(self, text):
...         self.text += text
...

If we make a new instance of this class, and we pass that instance to the print file argument, we'll print to this fake file object:

>>> fake = FakeFile()
>>> print(1, 2, file=fake)
>>> fake.text
'1 2\n'

Python doesn't care whether it's a fully functional file, just that it has a write method.

And Python's csv.reader class accepts any iterable of iterables, which unfortunately includes strings:

>>> import csv
>>> list(csv.reader("hi,hey"))
[['h'], ['i'], ['', ''], ['h'], ['e'], ['y']]

It doesn't do what we want when we pass a string to it, but it does something.

So Python's ** syntax works with any object that has a keys method and a __getitem__ method to look up the values for those keys.

That does not mean that your mappings should only have a keys method and a __getitem__ method. If you ever make your own mapping objects, I would inherit from the Mapping or MutableMapping classes from Python's collections.abc module, or inherit from an existing mapping class:

>>> from collections.abc import Mapping, MutableMapping

Duck typing: embrace it but be cautious

This is a weird and kind of niche topic. Why should you care?

I'll give you three reasons.

First, when you accept objects that should obey a particular protocol, try to do like Python by practicing duck typing. Check the behavior of an object, but not the type of that object.

Also, note that our embrace of duck typing in Python can sometimes allow objects to be accepted in a particular context, even though they really don't work in that context, which can result in bugs that don't raise exceptions, but instead just give us garbage output.

Also, when you make your own objects that should obey a particular protocol (an iterable, a mapping, or a sequence, for example), try not to stop at the bare minimum. Python might be fine with it, but your users likely won't be.

So you should embrace duck typing, but also be cautious of its downsides.

This is a free preview of a premium screencast. You have 2 previews remaining.