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
andMutableMapping
- 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.
Get Your Code: Click here to download the free sample code that you’ll use to learn about mappings in Python.
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 MappingsIn 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:
>>> 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
:
>>> 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
:
>>> 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:
>>> 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:
>>> 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
:
>>> 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:
>>> 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:
>>> 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:
>>> 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:
>>> 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:
>>> 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:
>>> # 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:
>>> {[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:
>>> [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.
Note: There are aliases to these and other abstract base classes in the typing
module. However, these have been deprecated since Python 3.9 and shouldn’t be used.
These classes can be used to verify that an object is an instance of a mapping:
>>> 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:
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:
>>> 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:
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.
Note: You’ll need to start a new session if you’re using Python’s standard REPL each time you make changes to the class definition in pizza_menu.py
. You can’t write the import statement again in the same REPL session, as this won’t import the updated class. It’s also possible to use importlib
from the standard library to reload a module, but it’s easier to start a new REPL.
Alternatively, you can press F6 to reload the imported modules if you’re using a different REPL such as bpython.
You can test the class in a new REPL session:
>>> 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:
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
.
Note: Some programmers consider it more Pythonic to get the class name using a more concise type(self).__name__
expression instead of directly accessing the .__class__
attribute.
You can confirm the output from these methods in a new REPL session:
>>> 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()
:
- No arguments: You create an empty dictionary when you call
dict()
with no arguments. - Mapping: You can use any mapping as an argument in
dict()
, which creates a new dictionary from the mapping. - 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. **kwargs
: You can use any number of keyword arguments when callingdict()
. The keywords become dictionary keys, and the argument values become dictionary values.
You can replicate this flexible approach directly in PizzaMenu
:
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:
>>> 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:
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()
:
>>> 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:
>>> 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__()
:
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:
>>> 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:
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:
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:
>>> 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:
>>> 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:
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:
>>> 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:
>>> 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:
>>> 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:
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:
>>> 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:
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__()
:
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
. Thiskey
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 aValueError
. - 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:
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:
>>> 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:
>>> 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:
>>> 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:
>>> 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:
>>> 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
:
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
:
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 thedict
initializer usingsuper()
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:
>>> 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:
>>> 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
:
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:
- The
if
statement inPizzaMenu.__getitem__()
checks whetherkey
is a member ofself._menu
. However, the equivalent conditional statement inPizzaMenuDict.__getitem__()
checks for membership directly inself
. - The
return
statement inPizzaMenu.__getitem__()
returnsself._menu[key]
. However, the final line inPizzaMenuDict.__getitem__()
calls and returns the superclass’s.__getitem__()
special method using the modifiedkey
. The superclass isdict
.
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__()
:
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:
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:
>>> 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
:
>>> 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
:
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:
>>> 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
andMutableMapping
- 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.
Get Your Code: Click here to download the free sample code that you’ll use to learn about mappings in Python.
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 MappingsIn 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.