Python Topics : Dictionaries
Getting Started With Python Dictionaries
a dictionary is a mutable collection of key-value pairs
the globals() function returns a dictionary containing key-value pairs which map names to objects in the current global scope
>>> globals()
{
    '__name__': '__main__',
    '__doc__': None,
    '__package__': None,
    ...
}
dictionaries are used to support the internal implementation of classes
>>> class Number:
...     def __init__(self, value):
...         self.value = value
...

>>> Number(42).__dict__
{'value': 42}
the .__dict__ special attribute is a dictionary which maps attribute names to their corresponding values in classes and objects
this implementation makes attribute and method lookup fast and efficient in object-oriented code

dictionaries have the following characteristics

  • mutable - the dictionary values can be updated in place
  • dynamic - dictionaries can grow and shrink as needed
  • efficient - implemented as hash tables allowing for fast key lookups
  • ordered - starting with Python 3.7, dictionaries keep their items in the same order they were inserted
couple of restrictions involving dictionary keys
  • hashable - can't use unhashable objects like lists as dictionary keys
  • unique - dictionaries can't have duplicate keys
values in a dictionary aren't restricted
can be of any Python type
Creating Dictionaries in Python
Dictionary Literals
syntax for a dictionary literal
{
    <key_1>: <value_1>,
    <key_2>: <value_2>,
          ...,
    <key_N>: <value_N>,
}
can create empty dictionary using just curly braces
d = {}
keys must be hashable
immutable types are hashable
mutable types are not hashable
can even use objects like data types and functions as keys
>>> types = {int: 1, float: 2, bool: 3}
>>> types
{<class 'int'>: 1, <class 'float'>: 2, <class 'bool >: 3}
>>> types[float]
2
>>> types[bool]
3
The dict() Constructor
c'tor signatures
dict()
dict(**kwargs)
dict(mapping, **kwargs)
dict(iterable, **kwargs)
if the keys are strings they can be specified as keyword args
>>> MLB_teams = dict(
...     Colorado="Rockies",
...     Chicago="White Sox",
...     Boston="Red Sox",
...     Minnesota="Twins",
...     Milwaukee="Brewers",
...     Seattle="Mariners",
... )
can also create a dictionary from an iterable of key-value pairs
>>> MLB_teams = dict(
...     [
...         ("Colorado", "Rockies"),
...         ("Chicago", "White Sox"),
...         ("Boston", "Red Sox"),
...         ("Minnesota", "Twins"),
...         ("Milwaukee", "Brewers"),
...         ("Seattle", "Mariners"),
...     ]
... )
to create dictionaries from sequences of values combine them with the built-in zip() function and then call dict()
>>> places = [
...     "Colorado",
...     "Chicago",
...     "Boston",
...     "Minnesota",
...     "Milwaukee",
...     "Seattle",
... ]

>>> teams = [
...     "Rockies",
...     "White Sox",
...     "Red Sox",
...     "Twins",
...     "Brewers",
...     "Mariners",
... ]

>>> dict(zip(places, teams))
{
    'Colorado': 'Rockies',
    'Chicago': 'White Sox',
    'Boston': 'Red Sox',
    'Minnesota': 'Twins',
    'Milwaukee': 'Brewers',
    'Seattle': 'Mariners'
}
the zip() function takes one or more iterables as arguments
returns tuples that combine items from each iterable

Using the .fromkeys() Class Method
the dict data type has a class method named .fromkeys()
create new dictionaries from an iterable of keys and a default value
the method's signature
.fromkeys(iterable, value=None, /)
example
>>>inventory = dict.fromkeys(["apple", "orange", "banana", "mango"], 0)

>>>inventory
{'apple': 0, 'orange': 0, 'banana': 0, 'mango': 0}
Accessing Dictionary Values
can access its content by keys
to retrieve a value from a dictionary
>>>MLB_teams = dict(
...     [
...         ("Colorado", "Rockies"),
...         ("Chicago", "White Sox"),
...         ("Boston", "Red Sox"),
...         ("Minnesota", "Twins"),
...         ("Milwaukee", "Brewers"),
...         ("Seattle", "Mariners"),
...     ]
... )
>>>MLB_teams["Minnesota"]
'Twins'
>>>MLB_teams["Colorado"]
'Rockies'
trying to access a key that doesn't exist raises a KeyError exception
>>>person = {
...     "first_name": "John",
...     "last_name": "Doe",
...     "age": 35,
...     "spouse": "Jane",
...     "children": ["Ralph", "Betty", "Bob"],
...     "pets": {"dog": "Frieda", "cat": "Sox"},
... }
dictionary contains a list and dictionary as part of its values
to access the nested list elements, can use the corresponding key and then the desired index
to access a key-value pair in a nested dictionary, you can use the outer key and then the inner key
>>>person["children"][0]
'Ralph'
>>>person["children"][2]
'Bob'

>>>person["pets"]["dog"]
'Frieda'
>>>person["pets"]["cat"]
'Sox'
Populating Dictionaries Incrementally
Assigning Keys Manually
create an empty dictionary with an empty pair of curly braces
then add new key-value pairs one at a time
>>>person = {}

>>>person["first_name"] = "John"
>>>person["last_name"] = "Doe"
>>>person["age"] = 35
>>>person["spouse"] = "Jane"
>>>person["children"] = ["Ralph", "Betty", "Bob"]
>>>person["pets"] = {"dog": "Frieda", "cat": "Sox"}

>>>person
{
    'first_name': 'John',
    'last_name': 'Doe',
    'age': 35,
    'spouse': 'Jane',
    'children': ['Ralph', 'Betty', 'Bob'],
    'pets': {'dog': 'Frieda', 'cat': 'Sox'}
}
Adding Keys in a for Loop
a for loop is a good approach for populating an empty dictionary with new data
>>>squares = {}

>>>for integer in range(1, 10):
...     squares[integer] = integer**2
...

>>>squares
{1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}
Building Dictionaries With Comprehensions
can create the square dictionary with a comprehension
>>>squares = {integer: integer**2 for integer in range(1, 10)}
>>>squares
{1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}
Exploring the dict Class Methods
Retrieving Data From Dictionaries
the .items() method returns a dictionary view containing tuples of keys and values
the first item in each tuple is the key
the second item is the associated value
>>>inventory = {"apple": 100, "orange": 80, "banana": 100}
>>>inventory.items()
dict_items([('apple', 100), ('orange', 80), ('banana', 100)])
Adding Key-Value Pairs and Updating Dictionaries
the .setdefault() method can set default values to keys
if key is in the dictionary, then the method returns the associated value
if key isn't in the dictionary, it's inserted with default as its associated value
returns default value None
>>>inventory = {"apple": 100, "orange": 80}

>>>inventory.setdefault("apple")
100
>>>print(inventory.setdefault("mango"))
None
>>>inventory
{'apple': 100, 'orange': 80, 'mango': None}
>>>inventory.setdefault("banana", 0)
0
>>>inventory
{'apple': 100, 'orange': 80, 'mango': None, 'banana': 0}
the .update() method merges a dictionary with another dictionary or with an iterable of key-value pairs
if other is a dictionary, then a_dict.update(other) merges the entries from other into a_dict
  • if the key isn't present in a_dict, then the key-value pair from other is added to a_dict
  • if the key is present in a_dict, then the corresponding value in a_dict is updated to the value in other
>>>config = {
...     "color": "green",
...     "width": 42,
...     "height": 100,
...     "font": "Courier",
... }

>>>user_config = {
...     "path": "/home",
...     "color": "red",
...     "font": "Arial",
...     "position": (200, 100),
... }

>>>config.update(user_config)

>>>config
{
    'color': 'red',
    'width': 42,
    'height': 100,
    'font': 'Arial',
    'path': '/home',
    'position': (200, 100)
}
the argument may also be a sequence of key-value pairs
>>> config.update([("width", 200), ("api_key", 1234)])
>>> config
{
    'color': 'red',
    'width': 200,
    'height': 100,
    'font': 'Arial',
    'path': '/home',
    'position': (200, 100),
    'api_key': 1234
}
can also call .update() with keyword arguments
>>> config.update(color="yellow", script="__main__.py")
>>> config
{
    'color': 'yellow',
    'width': 200,
    'height': 100,
    'font': 'Arial',
    'path': '/home',
    'position': (200, 100),
    'api_key': 1234,
    'script': '__main__.py'
}
Removing Data From Dictionaries
the .pop() method removes key-value pairs by keys
if the key exists, the method returns its associated value
associated value can be None
if the key doesn't exist and default isn't provided, a KeyError is raised
>>> inventory = {"apple": 100, "orange": 80, "banana": 100}

>>> inventory.pop("apple")
100
>>> inventory
{'orange': 80, 'banana': 100}

>>> inventory.pop("mango")
Traceback (most recent call last):
    ...
KeyError: 'mango'

>>> inventory.pop("mango", 0)
0
to just delete an item
>>> del inventory["banana"]
>>> inventory
{'orange': 80}
the .popitem() method removes a key-value pair from a dictionary
method returns the removed pair as a tuple of the form (key, value)
the pairs are removed in LIFO (last-in, first-out) order
if the dictionary is empty, then .popitem() raises a KeyError exception
>>> inventory = {"apple": 100, "orange": 80, "banana": 100}

>>> inventory.popitem()
('banana', 100)
>>> inventory
{'apple': 100, 'orange': 80}

>>> inventory.popitem()
('orange', 80)
>>> inventory
{'apple': 100}

>>> inventory.popitem()
('apple', 100)
>>> inventory
{}
calling the .clear() method on an existing dictionary will remove all the current key-value pairs
>>> inventory = {"apple": 100, "orange": 80, "banana": 100}
>>> inventory
{'apple': 100, 'orange': 80, 'banana': 100}
>>> inventory.clear()
>>> inventory
{}
Using Operators With Dictionaries
Membership: in and not in
the membership operators in and not in allow determining whether a given key, value, or item is in a dictionary
  • a key is in a dictionary, can use the dictionary itself or the .keys() method to provide the target iterable
  • a value is in a dictionary, can use the .values() method to provide the target iterable
  • an item is in a dictionary, can use the .items() method to provide the target iterable
Equality and Inequality: == and !=
the equality (==) and inequality (!=) operators work with dictionaries
these operators disregard element order when used with dictionaries
which is different from what happens with lists
>>> [1, 2, 3] == [3, 2, 1]
False
>>> {1: 1, 2: 2, 3: 3} == {3: 3, 2: 2, 1: 1}
True

>>> [1, 2, 3] != [3, 2, 1]
True
>>> {1: 1, 2: 2, 3: 3} != {3: 3, 2: 2, 1: 1}
False
when comparing a list using the equality operator, the result depends on both the content and the order
comparing two dictionaries that contain the same series of key-value pairs, the order of those pairs isn't considered
the inequality operator when used with dictionaries doesn't consider the order of pairs either

Union and Augmented Union: | and |=
The union operator (|) creates a new dictionary by merging the keys and values of two initial dictionaries
the values of the dictionary to the right of the operator take precedence when both dictionaries share keys
>>> default_config = {
...     "color": "green",
...     "width": 42,
...     "height": 100,
...     "font": "Courier",
... }

>>> user_config = {
...     "path": "/home",
...     "color": "red",
...     "font": "Arial",
...     "position": (200, 100),
... }

>>> config = default_config | user_config
>>> config
{
    'color': 'red',
    'width': 42,
    'height': 100,
    'font': 'Arial',
    'path': '/home',
    'position': (200, 100)
}
the augmented union operator (|=) updates an existing dictionary with key-value pairs from another dictionary, mapping, or iterable of key-value pairs
does not create a new dictionary
when the operands share keys, the values from the right-hand side operand take priority
>>> config = {
...     "color": "green",
...     "width": 42,
...     "height": 100,
...     "font": "Courier",
... }

>>> user_config = {
...     "path": "/home",
...     "color": "red",
...     "font": "Arial",
...     "position": (200, 100),
... }

>>> config |= user_config
>>> config
{
    'color': 'red',
    'width': 42,
    'height': 100,
    'font': 'Arial',
    'path': '/home',
    'position': (200, 100)
}
Use Built-in Functions With Dictionaries
FunctionDescription
all() returns True if all the items in an iterable are truthy and False otherwise
any() returns True if at least one element in the iterable is truthy and False otherwise
len() returns an integer representing the number of items in the input object
max() returns the largest value in an iterable or series of arguments
min() returns the smallest value in an iterable or series of arguments
sortedreturns a new sorted list of the elements in the iterable
sum() returns the sum of a start value and the values in the input iterable from left to right

Checking for Truthy Data in Dictionaries: all() and any()
have a dictionary which maps products to their amounts
want to know whether all of the products are stocked
can use the all() function with the dictionary values as a target
>>> inventory = {"apple": 100, "orange": 80, "banana": 100, "mango": 200}

>>> all(inventory.values())
True

>>> # Update the stock
>>> inventory["mango"] = 0
>>> all(inventory.values())
False

want to schedule interviews with candidates who meet any of the following criteria

  1. know Python already
  2. have five or more years of developer experience
  3. have a degree
example iterates the dictionary
# recruit_developer.py
def schedule_interview(applicant):
    print(f"Scheduled interview with {applicant['name']}")

applicants = [
    {
        "name": "Devon Smith",
        "programming_languages": ["c++", "ada"],
        "years_of_experience": 1,
        "has_degree": False,
        "email_address": "[email protected]",
    },
    {
        "name": "Susan Jones",
        "programming_languages": ["python", "javascript"],
        "years_of_experience": 2,
        "has_degree": False,
        "email_address": "[email protected]",
    },
    {
        "name": "Sam Hughes",
        "programming_languages": ["java"],
        "years_of_experience": 4,
        "has_degree": True,
        "email_address": "[email protected]",
    },
]
for applicant in applicants:
    knows_python = "python" in applicant["programming_languages"]
    experienced_dev = applicant["years_of_experience"] >= 5

    meets_criteria = (
        knows_python
        or experienced_dev
        or applicant["has_degree"]
    )
    if meets_criteria:
        schedule_interview(applicant)
example using any()
for applicant in applicants:
    knows_python = "python" in applicant["programming_languages"]
    experienced_dev = applicant["years_of_experience"] >= 5

    credentials = (
        knows_python,
        experienced_dev,
        applicant["has_degree"],
    )
    if any(credentials):
        schedule_interview(applicant)
Determining the Number of Dictionary Items: len()
when calling len() with a dictionary as an argument, the function returns the number of items in the dictionary

Finding Minimum and Maximum Values: min() and max()
>>> computer_parts = {
...     "CPU": 299.99,
...     "Motherboard": 149.99,
...     "RAM": 89.99,
...     "GPU": 499.99,
...     "SSD": 129.99,
...     "Power Supply": 79.99,
...     "Case": 99.99,
...     "Cooling System": 59.99,
... }

>>> min(computer_parts.values())
59.99
>>> max(computer_parts.values())
499.99
computer_parts.values() contains the prices
can also use the functions with dictionary keys and items
these functions are mostly used with numeric values

Sorting Dictionaries by Keys, Values, and Items: sorted()
Sorting a Dictionary

Summing Dictionary Values: sum()
can use the sum() function with dictionaries
use the function to sum up numeric dictionary values or keys
>>> daily_sales = {
...     "Monday": 1500,
...     "Tuesday": 1750,
...     "Wednesday": 1600,
...     "Thursday": 1800,
...     "Friday": 2000,
...     "Saturday": 2200,
...     "Sunday": 2100,
... }

>>> sum(daily_sales.values()) / len(daily_sales)
1850.0
Iterating Over Dictionaries
Traversing Dictionaries by Keys
can iterate over the keys of a dictionary either with the dictionary directly or the .keys() method
>>> students = {
...     "Alice": 89.5,
...     "Bob": 76.0,
...     "Charlie": 92.3,
...     "Diana": 84.7,
...     "Ethan": 88.9,
...     "Fiona": 95.6,
...     "George": 73.4,
...     "Hannah": 81.2,
... }

>>> for student in students:
...     print(student)
...
Alice
Bob
Charlie
Diana
Ethan
Fiona
George
Hannah

>>> for student in students.keys():
...     print(student)
...
Alice
Bob
Charlie
Diana
Ethan
Fiona
George
Hannah
Iterating Over Dictionary Values
can use the .values() method
>>> MLB_teams = {
...     "Colorado": "Rockies",
...     "Chicago": "White Sox",
...     "Boston": "Red Sox",
...     "Minnesota": "Twins",
...     "Milwaukee": "Brewers",
...     "Seattle": "Mariners",
... }

>>> for team in MLB_teams.values():
...     print(team)
...
Rockies
White Sox
Red Sox
Twins
Brewers
Mariners
can't access the keys when using .values()

Looping Through Dictionary Items
use the .items() method
>>> for place, team in MLB_teams.items():
...     print(place, "->", team)
...
Colorado -> Rockies
Chicago -> White Sox
Boston -> Red Sox
Minnesota -> Twins
Milwaukee -> Brewers
Seattle -> Mariners
Exploring Existing Dictionary-Like Classes
these classes and a few others are available in the collections module
ClassDescription
OrderedDict a dictionary subclass specially designed to remember the order of items
order is defined by the insertion order of the keys
OrderedDict isn't that useful anymore
since Python 3.6 dictionaries keep their items in the same insertion order
Counter a dictionary subclass specially designed to provide efficient counting capabilities
>>> from collections import Counter

>>> Counter("mississippi")
Counter({'i': 4, 's': 4, 'p': 2, 'm': 1})
example uses Counter to count the letters in a string
returns a dictionary
keys are the letters and the values are the number of occurrences of each letter
the items in a Counter instance are sorted in descending order out of the box
can be useful for building rankings
defaultdict a dictionary subclass specially designed to handle missing keys in dictionaries
automatically creates a new key and generates a default value when trying to access or modify a missing key
>>> employees = [
...     ("Sales", "John"),
...     ("Sales", "Martin"),
...     ("Accounting", "Kate"),
...     ("Marketing", "Elizabeth"),
...     ("Marketing", "Linda"),
... ]
want to create a dictionary that uses the departments as keys
each key should map a list of people working in the department
>>> from collections import defaultdict

>>> departments = defaultdict(list)

>>> for department, employee in employees:
...     departments[department].append(employee)
...

>>> departments
defaultdict(<class 'list'>,
    {
        'Sales': ['John', 'Martin'],
        'Accounting': ['Kate'],
        'Marketing': ['Elizabeth', 'Linda']
     }
)
create a defaultdict called departments
use a for loop to iterate through the employees list
the line departments[department].append(employee)
  1. creates the keys for the departments
  2. initializes them to an empty list if necessary
  3. appends the employees to each department
Creating Custom Dictionary-Like Classes
inherit from
  • the built-in dict class
  • the collections.UserDict class
the first approach may lead to some issues
it can work in situations where add functionality is wanted which doesn't imply changing the core functionality of dict
the second approach is more reliable and safe
can use it in most cases

create a dictionary-like class that has in-place sorting capabilities

class SortableDict(dict):
    def sort_by_keys(self, reverse=False):
        sorted_items = sorted(
            self.items(),
            key=lambda item: item[0],
            reverse=reverse
        )
        self.clear()
        self.update(sorted_items)

    def sort_by_values(self, reverse=False):
        sorted_items = sorted(
            self.items(),
            key=lambda item: item[1],
            reverse=reverse
        )
        self.clear()
        self.update(sorted_items)
inherits from the built-in dict class
added two new methods for sorting the dictionary by keys and values in place
these methods don't create a new dictionary object but modify the current one
output
>>> from sorted_dict import SortableDict

>>> students = SortableDict(
...     {
...         "Alice": 89.5,
...         "Bob": 76.0,
...         "Charlie": 92.3,
...         "Diana": 84.7,
...         "Ethan": 88.9,
...         "Fiona": 95.6,
...         "George": 73.4,
...         "Hannah": 81.2,
...     }
... )

>>> id(students)
4350960304

>>> students.sort_by_values(reverse=True)
>>> students
{
    'Fiona': 95.6,
    'Charlie': 92.3,
    'Alice': 89.5,
    'Ethan': 88.9,
    'Diana': 84.7,
    'Hannah': 81.2,
    'Bob': 76.0,
    'George': 73.4
}

>>> students.sort_by_keys()
>>> students
{
    'Alice': 89.5,
    'Bob': 76.0,
    'Charlie': 92.3,
    'Diana': 84.7,
    'Ethan': 88.9,
    'Fiona': 95.6,
    'George': 73.4,
    'Hannah': 81.2
}

>>> id(students)
4350960304
create an instance of SortableDict
call the .sort_by_values() method to sort the dictionary by grades in reverse order
call the .sort_by_keys() method to sort the dictionary alphabetically by student names
both methods modify the dictionary in-place
index