Nothing Special   »   [go: up one dir, main page]

Python Mappings: A Comprehensive Guide

Python Mappings: A Comprehensive Guide

by Stephen Gruppetta Jun 12, 2024 intermediate python

One of the main data structures you learn about early in your Python learning journey is the dictionary. Dictionaries are the most common and well-known of Python’s mappings. However, there are other mappings in Python’s standard library and third-party modules. Mappings share common characteristics, and understanding these shared traits will help you use them more effectively.

In this tutorial, you’ll learn about:

  • Basic characteristics of a mapping
  • Operations that are common to most mappings
  • Abstract base classes Mapping and MutableMapping
  • User-defined mutable and immutable mappings and how to create them

This tutorial assumes that you’re familiar with Python’s built-in data types, especially dictionaries, and with the basics of object-oriented programming.

Take the Quiz: Test your knowledge with our interactive “Python Mappings” quiz. You’ll receive a score upon completion to help you track your learning progress:


Interactive Quiz

Python Mappings

In this quiz, you'll test your understanding of the basic characteristics and operations of Python mappings. By working through this quiz, you'll revisit the key concepts and techniques of creating a custom mapping.

Understanding the Main Characteristics of Python Mappings

A mapping is a collection that allows you to look up a key and retrieve its value. The keys in mappings can be objects of a broad range of types. However, in most mappings, there are object types that can’t be used as keys, as you’ll learn later in this tutorial.

The previous paragraph described mappings as collections. A collection is an iterable container that has a defined size. However, mappings also have additional features. You’ll explore each of these mapping characteristics with examples from Python’s main mapping types.

The feature that’s most characteristic of mappings is the ability to retrieve a value using a key. You can use a dictionary to demonstrate this operation:

Python
>>> points = {
...     "Denise": 3,
...     "Igor": 2,
...     "Sarah": 3,
...     "Trevor": 1,
... }
>>> points["Sarah"]
3
>>> points["Matt"]
Traceback (most recent call last):
  ...
KeyError: 'Matt'

The dictionary points contains four items, each with a key and a value. You can use the key within the square brackets to fetch the value associated with that key. However, if the key doesn’t exist in the dictionary, the code raises a KeyError.

You can use one of the mappings in the standard-library collections module to assign a default value for keys that aren’t present in the collection. The defaultdict type includes a callable that’s called each time you try to access a key that doesn’t exist. If you want the default value to be zero, you can use a lambda function that returns 0 as the first argument in defaultdict:

Python
>>> from collections import defaultdict
>>> points_default = defaultdict(
...     lambda: 0,
...     points,
... )

>>> points_default
defaultdict(<function <lambda> at 0x104a95da0>, {'Denise': 3,
    'Igor': 2, 'Sarah': 3, 'Trevor': 1})
>>> points_default["Sarah"]
3
>>> points_default["Matt"]
0
>>> points_default
defaultdict(<function <lambda> at 0x103e6c700>, {'Denise': 3,
    'Igor': 2, 'Sarah': 3, 'Trevor': 1, 'Matt': 0})

The defaultdict constructor has two arguments in this example. The first argument is the callable that’s used when a default value is needed. The second argument is the dictionary you created earlier. You can use any valid argument when you call dict() as the second argument in defaultdict() or omit this argument to create an empty defaultdict.

When you access a key that’s missing from the dictionary, the key is added, and the default value is assigned to it. You can also create the same points_default object using the callable int as the first argument since calling int() with no arguments returns 0.

All mappings are also collections, which means they’re iterable containers with a defined length. You can explore these characteristics with another mapping in Python’s standard library, collections.Counter:

Python
>>> from collections import Counter
>>> letters = Counter("learning python")
>>> letters
Counter({'n': 3, 'l': 1, 'e': 1, 'a': 1, 'r': 1, 'i': 1, 'g': 1,
    ' ': 1, 'p': 1, 'y': 1, 't': 1, 'h': 1, 'o': 1})

The letters in the string "learning python" are converted into keys in Counter, and the number of occurrences of each letter is used as the value corresponding to each key.

You can confirm that this mapping is iterable, has a defined length, and is a container:

Python
>>> for letter in letters:
...     print(letter)
...
l
e
a
r
n
i
g

p
y
t
h
o

>>> len(letters)
13

>>> "n" in letters
True
>>> "x" in letters
False

You can use the Counter object letters in a for loop, which confirms it’s iterable. All mappings are iterable. However, the iteration loops through the keys and not the values. You’ll see how to iterate through the values or through both keys and values later in this tutorial.

The built-in len() function returns the number of items in the mapping. This is equal to the number of unique characters in the original string, including the space character. The object is sized since len() returns a value.

You can use the in keyword to confirm which elements are in the mapping. This check alone isn’t sufficient to confirm that the mapping is a container. However, you can also access the object’s .__contains__() special method directly:

Python
>>> letters.__contains__("n")
True

As you can see, the presence of this special method confirms that letters is a container.

The .__getitem__() Special Method in Mappings

The characteristics you learned about in the first section are defined using special methods within class definitions. Therefore, mappings have a .__iter__() special method to make them iterable, a .__contains__() special method to define them as containers, and a .__len__() special method to give them a size.

Mappings also have the .__getitem__() special method to make them subscriptable. An object is subscriptable when you can add square brackets after the object, such as my_object[item]. In a mapping, the value you use within the square brackets is the key in a key-value pair, and it’s used to fetch the value that corresponds to the key.

The .__getitem__() special method provides the interface for the square brackets notation. In Python’s dictionary and other mappings, this retrieval of data is implemented using a hash table, which makes data access efficient. You can read more about hash tables and how they’re implemented in Python’s dictionaries in Build a Hash Table in Python With TDD.

If you’re creating your own mapping, you’ll need to implement the .__getitem__() special method to retrieve values from keys. In most instances, the best option is to use Python’s dictionary or other mappings implemented in Python’s standard library to make use of the efficient data access already implemented in these data structures.

You’ll explore these ideas when you create a user-defined mapping later in this tutorial.

Keys, Values, and Items in Mappings

Return to one of the mappings you used earlier in this tutorial, the dictionary points:

Python
>>> points = {
...     "Denise": 3,
...     "Igor": 2,
...     "Sarah": 3,
...     "Trevor": 1,
... }

The dictionary consists of four keys that are associated with four values. Every mapping is characterized by these key-value pairs. Each key-value pair is one item. Therefore, this dictionary has four items. You can confirm this using len(points), which returns the integer 4.

Python mappings have three methods called .keys(), .values(), and .items(). You can start by exploring the first two of these:

Python
>>> points.keys()
dict_keys(['Denise', 'Igor', 'Sarah', 'Trevor'])

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

These methods are useful when you need to access only the keys or only the values in a dictionary. The .items() method returns the mapping’s items paired into tuples:

Python
>>> points.items()
dict_items([('Denise', 3), ('Igor', 2), ('Sarah', 3), ('Trevor', 1)])

The object returned by .items() is useful when you need to access the key-value pair as an iterable, for example, if you need to loop through the mapping and access both key and value for each item:

Python
>>> for name, number in points.items():
...     print(f"Number of points for {name}: {number}")
...
Number of points for Denise: 3
Number of points for Igor: 2
Number of points for Sarah: 3
Number of points for Trevor: 1

You can also confirm that other mappings have these methods:

Python
>>> from collections import Counter
>>> letters = Counter("learning python")
>>> letters
Counter({'n': 3, 'l': 1, 'e': 1, 'a': 1, 'r': 1, 'i': 1, 'g': 1,
    ' ': 1, 'p': 1, 'y': 1, 't': 1, 'h': 1, 'o': 1})

>>> letters.keys()
dict_keys(['l', 'e', 'a', 'r', 'n', 'i', 'g', ' ', 'p', 'y', 't',
    'h', 'o'])

>>> letters.values()
dict_values([1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1])

>>> letters.items()
dict_items([('l', 1), ('e', 1), ('a', 1), ('r', 1), ('n', 3),
    ('i', 1), ('g', 1), (' ', 1), ('p', 1), ('y', 1), ('t', 1),
    ('h', 1), ('o', 1)])

These methods do not return a list or a tuple. Instead, they return dict_keys, dict_values, or dict_items objects. Even the methods called on the Counter object return the same three data types since many mappings rely on the dict implementation.

The dict_keys, dict_values, and dict_items objects are dictionary views. These objects do not contain their own data, but they provide a view of the data stored within the mapping. To experiment with this idea, you can assign one of the views to a variable and then change the data in the original mapping:

Python
>>> values_in_points = points.values()
>>> values_in_points
dict_values([3, 2, 3, 1])

>>> points["Igor"] += 10

>>> values_in_points
dict_values([3, 12, 3, 1])

You assign the dict_values object returned by .values() to values_in_points. When you update the dictionary, the values in values_in_points also change.

The distinction between keys, values, and items is essential when working with mappings. You’ll revisit the methods to access these later in this tutorial when you create your own mapping.

Comparison Between Mappings, Sequences, and Sets

Earlier in this tutorial, you learned that a mapping is a collection in which you can access a value using a key that’s associated with it. Mappings aren’t the only collection in Python. Sequences and sets are also collections. Common sequences include lists, tuples, and strings.

It’s useful to understand the similarities and differences between these categories to understand mappings better. All collections are iterable containers that have a defined length. Objects that fall into any of these three categories share these characteristics.

Mappings and sequences are subscriptable. You can use the square bracket notation to access values from within mappings and sequences. This characteristic is defined by the .__getitem__() special method. Sets, on the other hand, can’t be subscripted:

Python
>>> # Mapping
>>> points = {
...     "Denise": 3,
...     "Igor": 2,
...     "Sarah": 3,
...     "Trevor": 1,
... }
>>> points["Igor"]
2

>>> # Sequence
>>> numbers = [4, 10, 34]
>>> numbers[1]
10

>>> # Set
>>> numbers_set = {4, 10, 34}
>>> numbers_set[1]
Traceback (most recent call last):
  ...
TypeError: 'set' object is not subscriptable

>>> numbers_set.__getitem__
Traceback (most recent call last):
  ...
AttributeError: 'set' object has no attribute '__getitem__'.
    Did you mean: '__getstate__'?

You can use the square brackets notation to access values from a dictionary and a list. The same applies to all mappings and sequences. But since sets don’t have the .__getitem__() special method, they can’t be subscripted.

However, there are differences between mappings and sequences when using the square brackets notation. Sequences are ordered structures, and the square brackets notation enables indexing using integers that represent the item’s position in the sequence. With sequences, you can also include a slice in the square brackets. Only integers and slices are allowed when subscripting a sequence.

Mappings don’t have to be ordered, and you can’t use the square brackets notation to access an item based on its position in the structure. Instead, you use the key in a key-item pair within the square brackets. Also, the objects you can use within the square brackets in a mapping aren’t restricted to integers and slices.

However, for most mappings, you can’t use mutable objects or immutable structures that contain mutable objects. This requirement is imposed by the hash table used to implement dictionaries and other mappings:

Python
>>> {[0, 0]: None, [1, 1]: None}
Traceback (most recent call last):
  ...
TypeError: unhashable type: 'list'

>>> from collections import Counter
>>> number_groups = ([1, 2, 3], [1, 2, 3], [2, 3, 4])
>>> Counter(number_groups)
Traceback (most recent call last):
  ...
TypeError: unhashable type: 'list'

As shown in this example, you can’t use a list as a key in a dictionary since lists are mutable and not hashable. And even though number_groups is a tuple, you can’t use it to create a Counter object since the tuple contains lists.

Mappings are not an ordered data structure. However, items in dictionaries maintain the order in which they were added. This feature has been present since Python 3.6, and it was added to the formal language description in Python 3.7. Even though the order of dictionary items is maintained, dictionaries aren’t an ordered structure like sequences. Here’s a demonstration of this difference:

Python
>>> [1, 2] == [2, 1]
False
>>> {"one": 1, "two": 2} == {"two": 2, "one": 1}
True

The two lists are not equal since the values are in different positions. However, the two dictionaries are equal because they have the same key-value pairs, even though the order in which they’re included is different.

In most mappings that are based on Python’s dictionary, keys have to be unique. This is another requirement of the hash table used to implement dictionaries and other mappings. However, items in sequences don’t have to be unique. Many sequences have repeated values.

The requirement to use unique hashable objects as keys in Python mappings comes from the implementation of dictionaries. It’s not a requirement that’s inherent to mappings. However, most mappings are built on top of Python’s dictionary and therefore, share the same requirement.

Sets also have unique values that must be hashable. Set items share a lot in common with dictionary keys since they’re also implemented using a hash table. However, items in a set don’t have a key-value pair, and you can’t access an element of a set using the square brackets notation.

Exploring the Mapping and MutableMapping Abstract Base Classes

Python has abstract base classes that define interfaces for data type categories such as mappings. In this section, you’ll learn about the Mapping and MutableMapping abstract base classes, which you’ll find in the collections.abc module.

These classes can be used to verify that an object is an instance of a mapping:

Python
>>> from collections import Counter
>>> from collections.abc import Mapping, MutableMapping
>>> points = {
...     "Denise": 3,
...     "Igor": 2,
...     "Sarah": 3,
...     "Trevor": 1,
... }

>>> isinstance(points, Mapping)
True
>>> isinstance(points, MutableMapping)
True

>>> letters = Counter("learning python")
>>> isinstance(letters, MutableMapping)
True

The dictionary points is a Mapping and a MutableMapping. All MutableMapping objects are also Mapping objects. The Counter object also returns True when you check whether it’s a MutableMapping. You could check whether points is a dict and letters is a Counter object instead.

However, if all you need is for an object to be a mapping, it’s preferable to use the abstract base classes. This idea fits well with Python’s duck typing philosophy since you’re checking what an object can do rather than what type it is.

The abstract base classes can also be used for type hinting and to create custom mappings through inheritance. However, when you need to create a user-defined mapping, you also have other options, which you’ll read about later in this tutorial.

Characteristics of the Mapping Abstract Base Class

The Mapping abstract base class defines the interface for all mappings by providing several methods and ensuring that required special methods are included.

The required special methods you need to define when you create a mapping are the following:

  • .__getitem__(): Defines how to access values using the square brackets notation.
  • .__iter__(): Defines how to iterate through the mapping.
  • .__len__(): Defines the size of the mapping.

The Mapping abstract base class also provides the following methods:

  • .__contains__: Defines how to determine membership of the mapping.
  • .__eq__(): Defines how to determine equality of two objects.
  • .__ne__(): Defines how to determine when two objects are not equal.
  • .keys(): Defines how to access the keys in the mapping.
  • .values(): Defines how to access the values in the mapping.
  • .items(): Defines how to access the key-value pairs in the mapping.
  • .get(): Defines an alternative way to access values using keys. This method allows you to set a default value to use if the key isn’t present in the mapping.

Every mapping in Python includes at least these methods. In the following section, you’ll learn about the methods also included in mutable mappings.

Characteristics of the MutableMapping Abstract Base Class

The Mapping abstract base class doesn’t include any methods needed to make changes to the mapping. It creates an immutable mapping. However, there’s a second abstract base class called MutableMapping to create the mutable version.

MutableMapping inherits from Mapping. Therefore, it includes all the methods present in Mapping, but has two additional required special methods:

  • .__setitem__(): Defines how to set a new value for a key.
  • .__delitem__(): Defines how to delete an item in the mapping.

The MutableMapping abstract base class also adds these methods:

  • .pop(): Defines how to remove a key from a mapping and return its value.
  • .popitem(): Defines how to remove and return the most recently added item in a mapping.
  • .clear(): Defines how to remove all the items from the mapping.
  • .update(): Defines how to update a dictionary using data passed as an argument to this method.
  • .setdefault(): Defines how to add a key with a default value if the key isn’t already in the mapping.

You may be familiar with many of these methods from using Python’s dict data structure. You’ll find these methods in all mutable mappings. Mappings can also have other methods in addition to this set. For example, a Counter object has a .most_common() method, which returns that most common key.

In the rest of this tutorial, you’ll use Mapping and MutableMapping to create a custom mapping and use many of these methods.

Creating a User-Defined Mapping

In the following sections of this tutorial, you’ll create a custom class for a user-defined mapping. You’ll create a class to create menu items for a local pizzeria. The restaurant owner has noticed that many customers order the wrong pizzas and then complain. Part of the problem is that several menu items use unfamiliar names, and customers often make errors with pizza names that start with the same letter.

The pizzeria owner has decided to only include pizzas that start with different letters. You’ll create a mapping to ensure that keys don’t start with the same letter. You should also be able to access the value associated with each key by using the full key or just the first letter. Therefore, menu["Margherita"] and menu["m"] will return the same value. The value linked to each key is the price of the pizza.

In this section, you’ll create a class that inherits from the Mapping abstract base class. This will give you a good insight into how mappings work. In later sections, you’ll work on other ways to create the same class.

You can start by creating a new class that inherits from Mapping and accepts a dictionary as its only argument:

Python pizza_menu.py
from collections.abc import Mapping

class PizzaMenu(Mapping):
    def __init__(self, menu: dict):
        self._menu = menu

The class PizzaMenu inherits from Mapping and its .__init__() special method accepts a dictionary, which is assigned to the ._menu data attribute. The leading underscore in ._menu indicates that this attribute is not meant to be accessed outside the class.

You can test this class in a REPL session:

Python
>>> from pizza_menu import PizzaMenu
>>> menu = PizzaMenu({"Margherita": 9.5, "Pepperoni": 10.5})
Traceback (most recent call last):
  ...
TypeError: Can't instantiate abstract class PizzaMenu without
    an implementation for abstract methods '__getitem__',
    '__iter__', '__len__'

You attempt to create an instance of PizzaMenu using a dictionary containing two items. But the code raises a TypeError. Since PizzaMenu inherits from the Mapping abstract base class, it must have the three required special methods .__getitem__(), .__iter__(), and .__len__().

The PizzaMenu object includes the ._menu data attribute, which is a dictionary. Therefore, you can use the properties of this dictionary to define these special methods:

Python pizza_menu.py
from collections.abc import Mapping

class PizzaMenu(Mapping):
    def __init__(self, menu: dict):
        self._menu = menu

    def __getitem__(self, key):
        return self._menu[key]

    def __iter__(self):
        return iter(self._menu)

    def __len__(self):
        return len(self._menu)

You define .__getitem__() so that when you access the value of a key in PizzaMenu, the object returns the value matching that key in the ._menu dictionary. The definition of .__iter__() ensures that iterating through a PizzaMenu object is equivalent to iterating through the ._menu dictionary, and .__len__() defines the size of the PizzaMenu object as the size of the ._menu dictionary.

You can test the class in a new REPL session:

Python
>>> from pizza_menu import PizzaMenu
>>> menu = PizzaMenu({"Margherita": 9.5, "Pepperoni": 10.5})
>>> menu
<pizza_menu.PizzaMenu object at 0x102fb6b10>

This works now. You created an instance of PizzaMenu. However, the output when you display the object is not helpful. It’s a good practice to define the .__repr__() special method for a user-defined class, which provides a programmer-friendly string representation of the object. You can also define the .__str__() special method to provide a user-friendly string representation:

Python pizza_menu.py
from collections.abc import Mapping

class PizzaMenu(Mapping):
    # ...

    def __repr__(self):
        return f"{self.__class__.__name__}({self._menu})"

    def __str__(self):
        return str(self._menu)

The .__repr__() special method produces an output that can be used to re-create the object. You could use the class name directly within the string, but instead, you use self.__class__.__name__ to retrieve the class name dynamically. This version ensures the .__repr__() method also works as intended for subclasses of PizzaMenu.

You can confirm the output from these methods in a new REPL session:

Python
>>> from pizza_menu import PizzaMenu
>>> menu = PizzaMenu({"Margherita": 9.5, "Pepperoni": 10.5})
>>> menu
PizzaMenu({'Margherita': 9.5, 'Pepperoni': 10.5})
>>> print(menu)
{'Margherita': 9.5, 'Pepperoni': 10.5}

You create an instance of the class from a dictionary and display the object. Click below to see an alternative .__init__() special method for PizzaMenu.

The .__init__() method you created for PizzaMenu accepts a dictionary as an argument. Therefore, you can only create a PizzaMenu from another dictionary. You can modify the .__init__() method to accept the same types of arguments you can use to create an instance of a dictionary when using dict().

There are four types of arguments you can use when creating a dictionary with dict():

  1. No arguments: You create an empty dictionary when you call dict() with no arguments.
  2. Mapping: You can use any mapping as an argument in dict(), which creates a new dictionary from the mapping.
  3. Iterable: You can use an iterable that has pairs of objects as an argument in dict(). The first item in each pair becomes the key, and the second item is its value in the new dictionary.
  4. **kwargs: You can use any number of keyword arguments when calling dict(). The keywords become dictionary keys, and the argument values become dictionary values.

You can replicate this flexible approach directly in PizzaMenu:

Python pizza_menu.py
from collections.abc import Mapping

class PizzaMenu(Mapping):
    def __init__(self, menu=None, /, **kwargs):
        self._menu = dict(menu or {}, **kwargs)

    # ...

This version allows you to create a PizzaMenu object in different ways. If keyword arguments are present, you call dict() with either menu or an empty dictionary as the first argument. The or keyword uses short-circuit evaluation so that menu is used if it’s truthy and the empty dictionary if menu is falsy. If no keyword arguments are present, you call dict() either with the first argument or with the empty dictionary if menu is missing:

Python
>>> from pizza_menu import PizzaMenu
>>> menu = PizzaMenu({"Margherita": 9.5, "Pepperoni": 10.5})
>>> menu
PizzaMenu({'Margherita': 9.5, 'Pepperoni': 10.5})

>>> menu = PizzaMenu(Margherita=9.5, Pepperoni=10.5)
>>> menu
PizzaMenu({'Margherita': 9.5, 'Pepperoni': 10.5})

>>> menu = PizzaMenu([("Margherita", 9.5), ("Pepperoni", 10.5)])
>>> menu
PizzaMenu({'Margherita': 9.5, 'Pepperoni': 10.5})

>>> menu = PizzaMenu()
>>> menu
PizzaMenu({})

You initialize a PizzaMenu instance using a dictionary, keyword arguments, a list of tuples, and finally, with no arguments.

You’ll use the simpler .__init__() special method in the rest of this tutorial to allow you to focus on other aspects of the mapping.

Your next step is to customize this class to fit the requirement that no keys start with the same letter.

Prevent Pizza Names Starting With the Same Letter

The pizzeria owner doesn’t want pizza names that start with the same letter. You choose to raise an exception if you try to create a PizzaMenu instance with invalid names:

Python pizza_menu.py
from collections.abc import Mapping

class PizzaMenu(Mapping):
    def __init__(self, menu: dict):
        self._menu = {}
        first_letters = set()
        for key, value in menu.items():
            first_letter = key[0].lower()
            if first_letter in first_letters:
                raise ValueError(
                    f"'{key}' is invalid."
                    " All pizzas must have unique first letters"
                )
            first_letters.add(first_letter)
            self._menu[key] = value

    # ...

You create a set called first_letters within .__init__(). As you loop through the dictionary, you convert the first letter to lowercase and check whether the letter is already in the set first_letters. Since no first letter can be repeated, the code in the loop raises an error if it finds a repeated letter.

If the code doesn’t raise an error, you add the first letter to the set to ensure there aren’t invalid names later in the iteration. You also add the value to the ._menu dictionary, which you initialize as an empty dictionary at the beginning of the .__init__() method.

You can verify this behavior in a new REPL session. You create a dictionary of proposed names to use as an argument for PizzaMenu():

Python
>>> from pizza_menu import PizzaMenu
>>> proposed_pizzas = {
...     "Margherita": 9.50,
...     "Pepperoni": 10.50,
...     "Hawaiian": 11.50,
...     "Meat Feast": 12.50,
...     "Capricciosa": 12.50,
...     "Napoletana": 11.50,
...     "Pizza Bianca": 10.50,
... }

>>> menu = PizzaMenu(proposed_pizzas)
Traceback (most recent call last):
  ...
ValueError: 'Meat Feast' is invalid. All pizzas must have
    unique first letters

The names in proposed_pizzas contain invalid entries. There are two pizzas that start with M and two that start with P. You can rename the pizzas and try again:

Python
>>> proposed_pizzas = {
...     "Margherita": 9.50,
...     "Pepperoni": 10.50,
...     "Hawaiian": 11.50,
...     "Feast of Meat": 12.50,
...     "Capricciosa": 12.50,
...     "Napoletana": 11.50,
...     "Bianca": 10.50,
... }

>>> menu = PizzaMenu(proposed_pizzas)
>>> menu
PizzaMenu({'Margherita': 9.5, 'Pepperoni': 10.5, 'Hawaiian': 11.5,
    'Feast of Meat': 12.5, 'Capricciosa': 12.5, 'Napoletana': 11.5,
    'Bianca': 10.5})

Now that there are no repeated first letters in the pizza names, you can create an instance of PizzaMenu.

If you’re offended by the inclusion of a Hawaiian pizza, read on. If you’re fine with pineapple on pizza, you can skip this section!

You can ensure no Hawaiian pizzas make it on your menu with an addition to .__init__():

Python pizza_menu.py
from collections.abc import Mapping

class PizzaMenu(Mapping):
    def __init__(self, menu: dict):
        self._menu = {}
        first_letters = set()
        for key, value in menu.items():
            if key.lower() in ("hawaiian", "pineapple"):
                raise ValueError(
                    "What?! Hawaiian pizza is not allowed"
                )
            first_letter = key[0].lower()
            if first_letter in first_letters:
                raise ValueError(
                    f"'{key}' is invalid."
                    " All pizzas must have unique first letters"
                )
            first_letters.add(first_letter)
            self._menu[key] = value

    # ...

You add an additional condition when iterating through the dictionary keys to exclude the Hawaiian pizza. You also ban pineapple pizza for good measure:

Python
>>> from pizza_menu import PizzaMenu
>>> proposed_pizzas = {
...     "Margherita": 9.50,
...     "Pepperoni": 10.50,
...     "Hawaiian": 11.50,
...     "Feast of Meat": 12.50,
...     "Capricciosa": 12.50,
...     "Napoletana": 11.50,
...     "Bianca": 10.50,
... }

>>> menu = PizzaMenu(proposed_pizzas)
Traceback (most recent call last):
  ...
ValueError: What?! Hawaiian pizza is not allowed

Hawaiian pizzas are now banned.

In the next section, you’ll add more functionality to PizzaMenu to allow you to access a value using either the full pizza name or just its first letter.

Add Alternative Way to Access Values in PizzaMenu

Since all pizzas have unique first letters, you can modify the class so you can use the first letter to access a value from PizzaMenu. For example, say you’d like to be able to use menu["Margherita"] or menu["m"] to access the price of a Margherita pizza.

You could add each first letter as a key in ._menu and assign it the same value as the key with the full pizza name. However, this duplicates data. You’d also need to be careful when you change the price of a pizza to ensure you change the value associated with the single-letter key.

Instead, you can create a dictionary that maps the first letter to the pizza name that starts with that letter. You’re already collecting the first letters of each pizza in a set to ensure there are no repetitions. You can refactor first_letter to be a dictionary instead of a set. Dictionary keys are also unique, so they can be used instead of a set:

Python pizza_menu.py
from collections.abc import Mapping

class PizzaMenu(Mapping):
    def __init__(self, menu: dict):
        self._menu = {}
        self._first_letters = {}
        for key, value in menu.items():
            first_letter = key[0].lower()
            if first_letter in self._first_letters:
                raise ValueError(
                    f"'{key}' is invalid."
                    " All pizzas must have unique first letters"
                )
            self._first_letters[first_letter] = key
            self._menu[key] = value

    # ...

You replace the set with a dictionary, and you define it as a data attribute of the instance since you’ll need to use this dictionary elsewhere in the class definition. You can still check whether the first letter of a pizza name is already in the dictionary. However, now you can also link the pizza’s full name by adding it as a value.

You also need to change .__getitem__() to enable the use of a single letter within the square brackets when you access a value from a PizzaMenu object:

Python pizza_menu.py
from collections.abc import Mapping

class PizzaMenu(Mapping):
    # ...

    def __getitem__(self, key):
        if key not in self._menu and len(key) > 1:
            raise KeyError(key)
        key = self._first_letters.get(key[0].lower(), key)
        return self._menu[key]

    # ...

The .__getitem__() special method can now accept a single-letter argument. If the argument assigned to the key parameter is not in ._menu and is not a single character, then you raise an exception since the key is not valid.

Then, you call .get() on self._first_letters, which is a dictionary. You include the parameter key as a default value in this call. If key is a single letter present in ._first_letters, .get() returns its value in this dictionary. This value is reassigned to key. However, if the argument of .__getitem__() isn’t an element of ._first_letters, the parameter key is unchanged since it’s the default value in .get().

You can confirm this change in a new REPL session:

Python
>>> from pizza_menu import PizzaMenu
>>> proposed_pizzas = {
...     "Margherita": 9.50,
...     "Pepperoni": 10.50,
...     "Hawaiian": 11.50,
...     "Feast of Meat": 12.50,
...     "Capricciosa": 12.50,
...     "Napoletana": 11.50,
...     "Bianca": 10.50,
... }

>>> menu = PizzaMenu(proposed_pizzas)

>>> menu["Margherita"]
9.5
>>> menu["m"]
9.5

The class is now more flexible. You can access the price of a pizza using its full name or just the first letter.

In the next section, you’ll explore other methods you expect to have in a mapping.

Override Other Methods Required for PizzaMenu

You learned about the characteristics that are common to all mappings earlier in this tutorial. Since PizzaMenu is a subclass of Mapping, it inherits methods that all mappings have.

You can check that a PizzaMenu object behaves as expected when you perform common operations:

Python
>>> from pizza_menu import PizzaMenu
>>> proposed_pizzas = {
...     "Margherita": 9.50,
...     "Pepperoni": 10.50,
...     "Hawaiian": 11.50,
...     "Feast of Meat": 12.50,
...     "Capricciosa": 12.50,
...     "Napoletana": 11.50,
...     "Bianca": 10.50,
... }

>>> menu = PizzaMenu(proposed_pizzas)
>>> another_menu = PizzaMenu(proposed_pizzas)

>>> menu is another_menu
False

>>> menu == another_menu
True

>>> for item in menu:
...     print(item)
...
Margherita
Pepperoni
Hawaiian
Feast of Meat
Capricciosa
Napoletana
Bianca

>>> "Margherita" in menu
True

>>> "m" in menu
True

You create two PizzaMenu objects from the same dictionary. The objects are different, and therefore, the is keyword returns False when comparing the two objects. However, the equality operator == returns True. So, the objects are equal if all the items in ._menu are equal. Since you haven’t defined .__eq__(), Python uses .__iter__() to iterate through both objects and compare their values.

In the REPL session, you also confirm that iterating through a PizzaMenu iterates through the keys, as with other mappings.

Finally, you confirm that you can verify whether a pizza name is a member of the PizzaMenu object using the in keyword. Since you haven’t defined .__contains__(), Python uses the .__getitem__() special method to look for the pizza name.

However, this also shows that the letter m is a member of the menu since you modified .__getitem__() to ensure you can use a single letter in the square brackets notation. If you prefer not to include single letters as members of the PizzaMenu object, you can define the .__contains__() special method:

Python pizza_menu.py
from collections.abc import Mapping

class PizzaMenu(Mapping):
    # ...

    def __contains__(self, key):
        return key in self._menu

When the .__contains__() method is present, Python uses it to check for membership. This bypasses .__getitem__() and checks whether the key is a member of the dictionary stored in ._menu. You can confirm that single letters are no longer considered members of the object in a new REPL session:

Python
>>> from pizza_menu import PizzaMenu
>>> proposed_pizzas = {
...     "Margherita": 9.50,
...     "Pepperoni": 10.50,
...     "Hawaiian": 11.50,
...     "Feast of Meat": 12.50,
...     "Capricciosa": 12.50,
...     "Napoletana": 11.50,
...     "Bianca": 10.50,
... }

>>> menu = PizzaMenu(proposed_pizzas)

>>> "Margherita" in menu
True

>>> "m" in menu
False

"Margherita" is still a member of the PizzaMenu object, but "m" is no longer a member.

You can also explore the methods that return the keys, values, and items of the mapping:

Python
>>> menu.keys()
KeysView(PizzaMenu({'Margherita': 9.5, 'Pepperoni': 10.5,
    'Hawaiian': 11.5, 'Feast of Meat': 12.5, 'Capricciosa': 12.5,
    'Napoletana': 11.5, 'Bianca': 10.5}))

>>> menu.values()
ValuesView(PizzaMenu({'Margherita': 9.5, 'Pepperoni': 10.5,
    'Hawaiian': 11.5, 'Feast of Meat': 12.5, 'Capricciosa': 12.5,
    'Napoletana': 11.5, 'Bianca': 10.5}))

>>> menu.items()
ItemsView(PizzaMenu({'Margherita': 9.5, 'Pepperoni': 10.5,
    'Hawaiian': 11.5, 'Feast of Meat': 12.5, 'Capricciosa': 12.5,
    'Napoletana': 11.5, 'Bianca': 10.5}))

The .keys(), .values(), and .items() methods exist since they’re inherited from the abstract base class, but they don’t display the expected values. Instead, they show the entire object, which is the string representation returned by .__repr__().

However, you can iterate through these views to fetch the correct values:

Python
>>> for key in menu.keys():
...    print(key)
...
Margherita
Pepperoni
Hawaiian
Feast of Meat
Capricciosa
Napoletana
Bianca

>>> for value in menu.values():
...     print(value)
...
9.5
10.5
11.5
12.5
12.5
11.5
10.5

>>> for item in menu.items():
...     print(item)
...
('Margherita', 9.5)
('Pepperoni', 10.5)
('Hawaiian', 11.5)
('Feast of Meat', 12.5)
('Capricciosa', 12.5)
('Napoletana', 11.5)
('Bianca', 10.5)

These methods work as intended, but their string representations don’t show the data you expect. You can override the .keys(), .values(), and .items() methods in the PizzaMenu class definition if you want to change this display, but it’s not necessary.

None of the methods in the Mapping abstract base class allow you to modify the contents of the mapping. This is an immutable mapping. In the next section, you’ll change PizzaMenu into a mutable mapping.

Creating a User-Defined Mutable Mapping

Earlier in this tutorial, you learned about the additional methods included in the MutableMapping interface. You can start converting the PizzaMenu class you created in the previous section into a mutable mapping by inheriting from the MutableMapping abstract base class without making any further changes for now:

Python pizza_menu.py
from collections.abc import MutableMapping

class PizzaMenu(MutableMapping):
    # ...

You can try to create an instance of this class in a new REPL session:

Python
>>> from pizza_menu import PizzaMenu
>>> proposed_pizzas = {
...     "Margherita": 9.50,
...     "Pepperoni": 10.50,
...     "Hawaiian": 11.50,
...     "Feast of Meat": 12.50,
...     "Capricciosa": 12.50,
...     "Napoletana": 11.50,
...     "Bianca": 10.50,
... }

>>> menu = PizzaMenu(proposed_pizzas)
Traceback (most recent call last):
  ...
TypeError: Can't instantiate abstract class PizzaMenu without an
    implementation for abstract methods '__delitem__', '__setitem__'

The immutable mapping you created in the previous section had three required special methods. Mutable mappings have two more: .__delitem__() and .__setitem__(). So you must include these when subclassing MutableMapping.

Change, Add, and Delete Items From the Pizza Menu

You can start by adding .__delitem__() to the class definition:

Python pizza_menu.py
from collections.abc import MutableMapping

class PizzaMenu(MutableMapping):
    # ...

    def __delitem__(self, key):
        if key not in self._menu and len(key) > 1:
            raise KeyError(key)
        key = self._first_letters.pop(key[0].lower(), key)
        del self._menu[key]

The .__delitem__() special method follows a similar pattern to .__getitem__(). If a key is not a single letter and is not a member of ._menu, the method raises a KeyError. Next, you call .first_letters.pop() and include key as the default value. The .pop() method removes and returns an item, but it returns the default value if the item isn’t in the dictionary.

Therefore, if a key is a single letter that’s in ._first_letters, it will be removed from this dictionary. The final line removes the pizza entry from ._menu. This removes the pizza name from both dictionaries.

The .__setitem__() method needs more discussion as there are several options you need to consider:

  • If you use the full name of an existing pizza when you set a new value, .__setitem__() should change the value of the existing item in ._menu.
  • If you use a single letter that matches an existing pizza, .__setitem__() should also change the value of the existing item in ._menu.
  • If you use a pizza name that doesn’t already exist in ._menu, the code needs to check for uniqueness of the first letter before adding the new item to ._menu.

You can include these points in the definition of .__setitem__():

Python pizza_menu.py
from collections.abc import MutableMapping

class PizzaMenu(MutableMapping):
    # ...

    def __setitem__(self, key, value):
        first_letter = key[0].lower()
        if len(key) == 1:
            key = self._first_letters.get(first_letter, key)
        if key in self._menu:
            self._menu[key] = value
        elif first_letter in self._first_letters:
            raise ValueError(
                f"'{key}' is invalid."
                " All pizzas must have unique first letters"
            )
        else:
            self._first_letters[first_letter] = key
            self._menu[key] = value

This method performs the following actions:

  • It assigns the key’s first letter to first_letter.
  • If the key is a single letter, it fetches the pizza’s full name and reassigns it to key. This key is used in the rest of this method. If there’s no matching pizza, key is unchanged to allow a new pizza with a single-letter name to be added.
  • If the key is a full pizza name that’s in ._menu, the new value is assigned to this key.
  • If the key is not in ._menu but its first letter is in ._first_letters, then this pizza name is invalid since it starts with a letter that’s already used. The method raises a ValueError.
  • Finally, the remaining option is for a key that’s new and valid. The first letter is added to ._first_letters, and the pizza name and price are added as a key-value pair in ._menu.

Note how you’re repeating the code that raises the ValueError twice. You can avoid this repetition by adding a new method:

Python pizza_menu.py
from collections.abc import MutableMapping

class PizzaMenu(MutableMapping):
    def __init__(self, menu: dict):
        self._menu = {}
        self._first_letters = {}
        for key, value in menu.items():
            first_letter = key[0].lower()
            if first_letter in self._first_letters:
                self._raise_duplicate_key_error(key)
            self._first_letters[first_letter] = key
            self._menu[key] = value

    def _raise_duplicate_key_error(self, key):
        raise ValueError(
            f"'{key}' is invalid."
            " All pizzas must have unique first letters"
        )

    # ...

    def __setitem__(self, key, value):
        first_letter = key[0].lower()
        if len(key) == 1:
            key = self._first_letters.get(first_letter, key)
        if key in self._menu:
            self._menu[key] = value
        elif first_letter in self._first_letters:
            self._raise_duplicate_key_error(key)
        else:
            self._first_letters[first_letter] = key
            self._menu[key] = value

The new method ._raise_duplicate_key_error() can be called whenever there’s an invalid name. You use this in .__init__() and .__setitem__().

You can now try to mutate a PizzaMenu object in a new REPL session:

Python
>>> from pizza_menu import PizzaMenu
>>> proposed_pizzas = {
...     "Margherita": 9.50,
...     "Pepperoni": 10.50,
...     "Hawaiian": 11.50,
...     "Feast of Meat": 12.50,
...     "Capricciosa": 12.50,
...     "Napoletana": 11.50,
...     "Bianca": 10.50,
... }

>>> menu = PizzaMenu(proposed_pizzas)
>>> menu
PizzaMenu({'Margherita': 9.5, 'Pepperoni': 10.5, 'Hawaiian': 11.5,
    'Feast of Meat': 12.5, 'Capricciosa': 12.5, 'Napoletana': 11.5,
    'Bianca': 10.5})

>>> menu["m"] = 10.25
>>> menu
PizzaMenu({'Margherita': 10.25, 'Pepperoni': 10.5, 'Hawaiian': 11.5,
'Feast of Meat': 12.5, 'Capricciosa': 12.5, 'Napoletana': 11.5,
'Bianca': 10.5})

>>> menu["Pepperoni"] += 1.25
>>> menu
PizzaMenu({'Margherita': 10.25, 'Pepperoni': 11.75, 'Hawaiian': 11.5,
    'Feast of Meat': 12.5, 'Capricciosa': 12.5, 'Napoletana': 11.5,
    'Bianca': 10.5})

Now you can change the value of an item using either a single letter or the full name when accessing it. You can also add new values to the mapping:

Python
>>> menu["Regina"] = 13
>>> menu
PizzaMenu({'Margherita': 10.25, 'Pepperoni': 11.75, 'Hawaiian': 11.5,
    'Feast of Meat': 12.5, 'Capricciosa': 12.5, 'Napoletana': 11.5,
    'Bianca': 10.5, 'Regina': 13})

The PizzaMenu mutable mapping has a new item, which is added at the end since dictionaries preserve the order of insertion. However, you can’t add a new pizza if it shares the same first letter as a pizza that’s already on the menu:

Python
>>> menu["Plain Pizza"] = 10
Traceback (most recent call last):
  ...
ValueError: 'Plain Pizza' is an invalid name. All pizzas must
    have unique first letters

You can also delete items from the mapping:

Python
>>> del menu["Hawaiian"]
>>> menu
PizzaMenu({'Margherita': 10.25, 'Pepperoni': 11.75, 'Feast of Meat': 12.5,
    'Capricciosa': 12.5, 'Napoletana': 11.5, 'Bianca': 10.5})

>>> menu["h"]
Traceback (most recent call last):
  ...
KeyError: 'h'

>>> menu["Hawaiian"]
Traceback (most recent call last):
  ...
KeyError: 'Hawaiian'

Once you remove the Hawaiian pizza, you get a KeyError when you try to access it, using either a single letter or the full name.

Use Other Methods That Mutate Mappings

The MutableMapping abstract base class also adds more methods to the class, such as .pop() and .update(). You can check whether these work as expected:

Python
>>> menu.pop("n")
11.5
>>> menu
PizzaMenu({'Margherita': 10.25, 'Pepperoni': 11.75,
    'Feast of Meat': 12.5, 'Capricciosa': 12.5, 'Bianca': 10.5})

>>> menu.update({"Margherita": 11.5, "c": 14})
>>> menu
PizzaMenu({'Margherita': 11.5, 'Pepperoni': 11.75,
    'Feast of Meat': 12.5, 'Capricciosa': 14, 'Bianca': 10.5})

>>> menu.update({"Festive Pizza": 16})
Traceback (most recent call last):
  ...
ValueError: 'Festive Pizza' is an invalid name. All pizzas must
    have unique first letters

You can use a single letter in .pop(), which removes the Napoletana pizza. The .update() method also works with full pizza names or single letters. The price of the Capricciosa pizza is updated since you include the key "c" when you call .update().

You also can’t use .update() to add an invalid pizza name. The Festive Pizza was rejected since there’s already another pizza name that starts with F.

This shows that you don’t need to define all these methods, as the special methods you already defined may be sufficient. As an exercise, you can verify that the methods added by MutableMapping don’t need overriding in this example.

Here’s the final version of the PizzaMenu class that inherits from MutableMapping:

Python pizza_menu.py
from collections.abc import MutableMapping

class PizzaMenu(MutableMapping):
    def __init__(self, menu: dict):
        self._menu = {}
        self._first_letters = {}
        for key, value in menu.items():
            first_letter = key[0].lower()
            if first_letter in self._first_letters:
                self._raise_duplicate_key_error(key)
            self._first_letters[first_letter] = key
            self._menu[key] = value

    def _raise_duplicate_key_error(self, key):
        raise ValueError(
            f"'{key}' is invalid."
            " All pizzas must have unique first letters"
        )

    def __getitem__(self, key):
        if key not in self._menu and len(key) > 1:
            raise KeyError(key)
        key = self._first_letters.get(key[0].lower(), key)
        return self._menu[key]

    def __setitem__(self, key, value):
        first_letter = key[0].lower()
        if len(key) == 1:
            key = self._first_letters.get(first_letter, key)
        if key in self._menu:
            self._menu[key] = value
        elif first_letter in self._first_letters:
            self._raise_duplicate_key_error(key)
        else:
            self._first_letters[first_letter] = key
            self._menu[key] = value

    def __delitem__(self, key):
        if key not in self._menu and len(key) > 1:
            raise KeyError(key)
        key = self._first_letters.pop(key[0].lower(), key)
        del self._menu[key]

    def __iter__(self):
        return iter(self._menu)

    def __len__(self):
        return len(self._menu)

    def __repr__(self):
        return f"{self.__class__.__name__}({self._menu})"

    def __str__(self):
        return str(self._menu)

    def __contains__(self, key):
        return key in self._menu

This version contains all the methods discussed in this section of the tutorial.

You’ll write another version of this class in the next section of this tutorial.

Inheriting From dict and collections.UserDict

When you inherit from Mapping or MutableMapping, you need to define all the required methods. Mapping requires you to define at least .__getitem__(), .__iter__(), and .__len__(), and MutableMapping also requires .__setitem__() and .__delitem__(). You have full control when defining the mapping.

In the previous sections, you created a class from these abstract base classes. This is useful to help understand what happens within a mapping.

However, when you create a custom mapping, you often want to model it on a dictionary. There are other options for creating custom mappings. In the following section, you’ll re-create the custom mapping for the pizza menu by inheriting directly from dict.

A Class That Inherits From dict

In earlier Python versions, it wasn’t possible to subclass built-in types like dict. However, this is no longer the case. Still, there are challenges when inheriting from dict.

You can start re-creating the class to inherit from dict and define the first two methods. All methods you define will be similar to the ones in the previous section but will have small and important differences. You can distinguish the class name by calling the new class PizzaMenuDict:

Python pizza_menu.py
class PizzaMenuDict(dict):
    def __init__(self, menu: dict):
        _menu = {}
        self._first_letters = {}
        for key, value in menu.items():
            first_letter = key[0].lower()
            if first_letter in self._first_letters:
                self._raise_duplicate_key_error(key)
            self._first_letters[first_letter] = key
            _menu[key] = value
        super().__init__(_menu)

    def _raise_duplicate_key_error(self, key):
        raise ValueError(
            f"'{key}' is invalid."
            " All pizzas must have unique first letters"
        )

The class now inherits from dict. The ._raise_duplicate_key_error() is identical to the version in PizzaMenu you wrote earlier. The .__init__() method has some changes:

  • The internal dictionary is no longer a data attribute self._menu but a local variable _menu since it’s no longer needed elsewhere in the class.
  • This local variable ._menu is passed to the dict initializer using super() in the final line.

Since a PizzaMenuDict object is a dictionary, you can access the dictionary’s data directly through the object using self within the methods. Any operations on self will use methods defined in PizzaMenuDict. However, if methods are not defined in PizzaMenuDict, the dict methods are used.

Therefore, PizzaMenuDict is now a dictionary which ensures there are no items starting with the same letter when initializing the object. It also has an additional data attribute, ._first_letters. You can confirm that initialization works as expected:

Python
>>> from pizza_menu import PizzaMenuDict
>>> menu = PizzaMenuDict({"Margherita": 9.5, "Pepperoni": 10.5})
>>> menu
{'Margherita': 9.5, 'Pepperoni': 10.5}

>>> menu = PizzaMenuDict({"Margherita": 9.5, "Meat Feast": 10.5})
Traceback (most recent call last):
  ...
ValueError: 'Meat Feast' is an invalid name.
    All pizzas must have unique first letters

You get an error when you attempt to create a PizzaMenuDict object with two pizzas starting with M. However, none of the other special methods are defined. Therefore, this class doesn’t have all the required features yet:

Python
>>> menu["m"]
Traceback (most recent call last):
  ...
KeyError: 'm'

You can’t access a value using a single letter. But you can implement the .__getitem__() method, which is similar but not identical to the method you defined in the previous section in PizzaMenu:

Python pizza_menu.py
class PizzaMenuDict(dict):
    # ...

    def __getitem__(self, key):
        if key not in self and len(key) > 1:
            raise KeyError(key)
        key = self._first_letters.get(key[0].lower(), key)
        return super().__getitem__(key)

There are two differences from the .__getitem__() in PizzaMenu in the previous section since the ._menu data attribute is no longer present in this version:

  1. The if statement in PizzaMenu.__getitem__() checks whether key is a member of self._menu. However, the equivalent conditional statement in PizzaMenuDict.__getitem__() checks for membership directly in self.
  2. The return statement in PizzaMenu.__getitem__() returns self._menu[key]. However, the final line in PizzaMenuDict.__getitem__() calls and returns the superclass’s .__getitem__() special method using the modified key. The superclass is dict.

So, the .__getitem__() method in PizzaMenuDict deals with the single letter case and then calls .__getitem__() in the dict class.

You’ll notice the same pattern in .__setitem__():

Python pizza_menu.py
class PizzaMenuDict(dict):
    # ...

    def __setitem__(self, key, value):
        first_letter = key[0].lower()
        if len(key) == 1:
            key = self._first_letters.get(first_letter, key)
        if key in self:
            super().__setitem__(key, value)
        elif first_letter in self._first_letters:
            self._raise_duplicate_key_error(key)
        else:
            self._first_letters[first_letter] = key
            super().__setitem__(key, value)

Whenever you need to update the data in the mapping, you call dict.__setitem__() instead of setting the values in the ._menu data attribute, as you did in PizzaMenu.

You also need to define .__delitem__() in a similar way:

Python pizza_menu.py
class PizzaMenuDict(dict):
    # ...

    def __delitem__(self, key):
        if key not in self and len(key) > 1:
            raise KeyError(key)
        key = self._first_letters.pop(key[0].lower(), key)
        super().__delitem__(key)

The last line in the method calls the .__delitem__() method in dict.

Note that you don’t need to define special methods such as .__repr__(), .__str__(), .__iter__(), or .__len__(), as you had to do when inheriting from the abstract base classes Mapping and MutableMapping. Since a PizzaMenuDict is a subclass of dict, you can rely on the dictionary methods if you don’t require different behavior. You’ll need to start a new REPL session since you made changes to the class definition:

Python
>>> from pizza_menu import PizzaMenuDict
>>> menu = PizzaMenuDict({"Margherita": 9.5, "Pepperoni": 10.5})

>>> for pizza in menu:
...     print(pizza)
...
Margherita
Pepperoni

>>> menu
{'Margherita': 9.5, 'Pepperoni': 10.5}

>>> len(menu)
2

Iteration uses the .__iter__() method in dict. String representation and finding the object’s length also work as expected since the .__repr__() and .__len__() special methods in dict are sufficient.

Methods That Need Updating

It seems as though less work is needed to inherit from dict. However, there are other methods you need to pay attention to. For example, you can explore .pop() with PizzaMenuDict:

Python
>>> menu.pop("m")
Traceback (most recent call last):
  ...
KeyError: 'm'

Since you haven’t defined .pop() in PizzaMenuDict, the class uses the dict method instead. However, dict.pop() uses dict.__getitem__(), so it bypasses the .__getitem__() method you defined specifically for PizzaMenuDict. You need to override .pop() in PizzaMenuDict:

Python pizza_menu.py
class PizzaMenuDict(dict):
    # ...

    def pop(self, key):
        key = self._first_letters.pop(key[0].lower(), key)
        return super().pop(key)

You ensure the key is always the pizza’s full name before calling and returning the superclass’s .pop() method. You can confirm this works in a new REPL session:

Python
>>> from pizza_menu import PizzaMenuDict
>>> menu = PizzaMenuDict({"Margherita": 9.5, "Pepperoni": 10.5})

>>> menu.pop("m")
9.5

>>> menu
{'Pepperoni': 10.5}

Now, you can use a single-letter argument in .pop().

You’ll need to go through all the dict methods to determine which ones need to be defined in PizzaMenuDict. You can try to complete this class as an exercise. You’ll notice that this process makes this approach longer and more error-prone. Therefore, in this pizzeria example, inheriting directly from MutableMapping may be the better option.

However, you may have other applications where you’re extending the functionality of a dictionary without changing any of its existing characteristics. Inheriting from dict may be the ideal option in those cases.

Another Alternative: collections.UserDict

In the collections module, you’ll find another class you can inherit from to create a dictionary-like object. You can inherit from UserDict instead of MutableMapping or dict. UserDict was included in Python when it was impossible to inherit directly from dict. However, UserDict is not entirely obsolete now that subclassing dict is possible.

UserDict creates a wrapper around a dictionary rather than subclassing dict. A UserDict object includes an attribute called .data, which is a dictionary containing the data. This attribute is similar to the ._menu attribute you added to PizzaMenu when inheriting from Mapping and MutableMapping.

However, UserDict is a concrete class, not an abstract base class. So, you don’t need to define the required special methods unless you require a different behavior.

You already wrote two versions of the class to create a menu for the pizzeria, so you won’t write a third one in this tutorial. There isn’t much more to learn about mappings by doing so. However, if you want to learn more about the similarities and differences between inheriting from dict or UserDict, you can read Custom Python Dictionaries: Inheriting From dict vs UserDict.

Conclusion

Python’s dictionary is the most commonly used mapping, and it’ll be suitable in most cases where you need a mapping. However, there are other mappings in the standard library and third-party libraries. You may also have applications where you need to create a custom mapping.

In this tutorial, you learned about:

  • Basic characteristics of a mapping
  • Operations that are common to most mappings
  • Abstract base classes Mapping and MutableMapping
  • User-defined mutable and immutable mappings and how to create them

Understanding the common traits across all mappings and what’s happening behind the scenes when you create and use mapping objects will help you use dictionaries and other mappings more effectively in your Python programs.

Take the Quiz: Test your knowledge with our interactive “Python Mappings” quiz. You’ll receive a score upon completion to help you track your learning progress:


Interactive Quiz

Python Mappings

In this quiz, you'll test your understanding of the basic characteristics and operations of Python mappings. By working through this quiz, you'll revisit the key concepts and techniques of creating a custom mapping.

🐍 Python Tricks 💌

Get a short & sweet Python Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Python Tricks Dictionary Merge

About Stephen Gruppetta

Stephen worked as a research physicist in the past, developing imaging systems to detect eye disease. He now focuses on Python education!

» More about Stephen

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Master Real-World Python Skills With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

Master Real-World Python Skills
With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Rate this article:

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students. Get tips for asking good questions and get answers to common questions in our support portal.


Looking for a real-time conversation? Visit the Real Python Community Chat or join the next “Office Hours” Live Q&A Session. Happy Pythoning!