Python dictionaries are something completely different (Monty Python reference intended)—they are not sequences at all, but are instead known as mappings. Mappings are also collections of other objects, but they store objects by key instead of by relative position. In fact, mappings don’t maintain any reliable left-to-right order; they simply map keys to associated values. Dictionaries, the only mapping type in Python’s core objects set, are also mutable: like lists, they may be changed in place and can grow and shrink on demand. Also like lists, they are a flexible tool for representing collections, but their more mnemonic keys are better suited when a collection’s items are named or labeled—fields of a database record, for example.
When written as literals, dictionaries are coded in curly braces and consist of a series of “key: value” pairs. Dictionaries are useful anytime we need to associate a set of values with keys—to describe the properties of something, for instance. As an example, consider the following three-item dictionary (with keys “food,” “quantity,” and “color,” perhaps the details of a hypothetical menu item?):
>>> D = {'food': 'Spam', 'quantity': 4, 'color': 'pink'}
We can index this dictionary by key to fetch and change the keys’ associated values. The dictionary index operation uses the same syntax as that used for sequences, but the item in the square brackets is a key, not a relative position:
>>>D['food']# Fetch value of key 'food' 'Spam' >>>D['quantity'] += 1# Add 1 to 'quantity' value >>>D{'color': 'pink', 'food': 'Spam', 'quantity': 5}
Although the curly-braces literal form does see use, it is perhaps more common to see dictionaries built up in different ways (it’s rare to know all your program’s data before your program runs). The following code, for example, starts with an empty dictionary and fills it out one key at a time. Unlike out-of-bounds assignments in lists, which are forbidden, assignments to new dictionary keys create those keys:
>>>D = {}>>>D['name'] = 'Bob'# Create keys by assignment >>>D['job'] = 'dev'>>>D['age'] = 40>>>D{'age': 40, 'job': 'dev', 'name': 'Bob'} >>>print(D['name'])Bob
Here, we’re effectively using dictionary keys as field names in a record that describes someone. In other applications, dictionaries can also be used to replace searching operations—indexing a dictionary by key is often the fastest way to code a search in Python.
As we’ll learn later, we can also make dictionaries by passing to the dict type name either keyword arguments (a special name=value syntax in function calls), or the result of zipping together sequences of keys and values obtained at runtime (e.g., from files). Both the following make the same dictionary as the prior example and its equivalent {} literal form, though the first tends to make for less typing:
>>>bob1 = dict(name='Bob', job='dev', age=40)# Keywords >>>bob1{'age': 40, 'name': 'Bob', 'job': 'dev'} >>>bob2 = dict(zip(['name', 'job', 'age'], ['Bob', 'dev', 40]))# Zipping >>>bob2{'job': 'dev', 'name': 'Bob', 'age': 40}
Notice how the left-to-right order of dictionary keys is scrambled. Mappings are not positionally ordered, so unless you’re lucky, they’ll come back in a different order than you typed them. The exact order may vary per Python, but you shouldn’t depend on it, and shouldn’t expect yours to match that in this book.
In the prior example, we used a dictionary to describe a hypothetical person, with three keys. Suppose, though, that the information is more complex. Perhaps we need to record a first name and a last name, along with multiple job titles. This leads to another application of Python’s object nesting in action. The following dictionary, coded all at once as a literal, captures more structured information:
>>>rec = {'name': {'first': 'Bob', 'last': 'Smith'},'jobs': ['dev', 'mgr'],'age': 40.5}
Here, we again have a three-key dictionary at the top (keys “name,” “jobs,” and “age”), but the values have become more complex: a nested dictionary for the name to support multiple parts, and a nested list for the jobs to support multiple roles and future expansion. We can access the components of this structure much as we did for our list-based matrix earlier, but this time most indexes are dictionary keys, not list offsets:
>>>rec['name']# 'name' is a nested dictionary {'last': 'Smith', 'first': 'Bob'} >>>rec['name']['last']# Index the nested dictionary 'Smith' >>>rec['jobs']# 'jobs' is a nested list ['dev', 'mgr'] >>>rec['jobs'][-1]# Index the nested list 'mgr' >>>rec['jobs'].append('janitor')# Expand Bob's job description in place >>>rec{'age': 40.5, 'jobs': ['dev', 'mgr', 'janitor'], 'name': {'last': 'Smith', 'first': 'Bob'}}
Notice how the last operation here expands the nested jobs list—because the jobs list is a separate piece of memory from the dictionary that contains it, it can grow and shrink freely (object memory layout will be discussed further later in this book).
The real reason for showing you this example is to demonstrate the flexibility of Python’s core data types. As you can see, nesting allows us to build up complex information structures directly and easily. Building a similar structure in a low-level language like C would be tedious and require much more code: we would have to lay out and declare structures and arrays, fill out values, link everything together, and so on. In Python, this is all automatic—running the expression creates the entire nested object structure for us. In fact, this is one of the main benefits of scripting languages like Python.
Just as importantly, in a lower-level language we would have to be careful to clean up all of the object’s space when we no longer need it. In Python, when we lose the last reference to the object—by assigning its variable to something else, for example—all of the memory space occupied by that object’s structure is automatically cleaned up for us:
>>> rec = 0 # Now the object's space is reclaimed
Technically speaking, Python has a feature known as garbage collection that cleans up unused memory as your program runs and frees you from having to manage such details in your code. In standard Python (a.k.a. CPython), the space is reclaimed immediately, as soon as the last reference to an object is removed. We’ll study how this works later in Chapter 6; for now, it’s enough to know that you can use objects freely, without worrying about creating their space or cleaning up as you go.
Also watch for a record structure similar to the one we just coded in Chapter 8, Chapter 9, and Chapter 27, where we’ll use it to compare and contrast lists, dictionaries, tuples, named tuples, and classes—an array of data structure options with tradeoffs we’ll cover in full later.[14]
As mappings, dictionaries support accessing items by key only, with the sorts of operations we’ve just seen. In addition, though, they also support type-specific operations with method calls that are useful in a variety of common use cases. For example, although we can assign to a new key to expand a dictionary, fetching a nonexistent key is still a mistake:
>>>D = {'a': 1, 'b': 2, 'c': 3}>>>D{'a': 1, 'c': 3, 'b': 2} >>>D['e'] = 99# Assigning new keys grows dictionaries >>>D{'a': 1, 'c': 3, 'b': 2, 'e': 99} >>>D['f']# Referencing a nonexistent key is an error...error text omitted...KeyError: 'f'
This is what we want—it’s usually a programming error to fetch something that isn’t really there. But in some generic programs, we can’t always know what keys will be present when we write our code. How do we handle such cases and avoid errors? One solution is to test ahead of time. The dictionary in membership expression allows us to query the existence of a key and branch on the result with a Python if statement. In the following, be sure to press Enter twice to run the if interactively after typing its code (as explained in Chapter 3, an empty line means “go” at the interactive prompt), and just as for the earlier multiline dictionaries and lists, the prompt changes to “...” on some interfaces for lines two and beyond:
>>>'f' in DFalse >>>if not 'f' in D:# Python's sole selection statementprint('missing')missing
This book has more to say about the if statement in later chapters, but the form we’re using here is straightforward: it consists of the word if, followed by an expression that is interpreted as a true or false result, followed by a block of code to run if the test is true. In its full form, the if statement can also have an else clause for a default case, and one or more elif (“else if”) clauses for other tests. It’s the main selection statement tool in Python; along with both its ternary if/else expression cousin (which we’ll meet in a moment) and the if comprehension filter lookalike we saw earlier, it’s the way we code the logic of choices and decisions in our scripts.
If you’ve used some other programming languages in the past, you might be wondering how Python knows when the if statement ends. I’ll explain Python’s syntax rules in depth in later chapters, but in short, if you have more than one action to run in a statement block, you simply indent all their statements the same way—this both promotes readable code and reduces the number of characters you have to type:
>>>if not 'f' in D:print('missing')print('no, really...')# Statement blocks are indented missing no, really...
Besides the in test, there are a variety of ways to avoid accessing nonexistent keys in the dictionaries we create: the get method, a conditional index with a default; the Python 2.X has_key method, an in work-alike that is no longer available in 3.X; the try statement, a tool we’ll first meet in Chapter 10 that catches and recovers from exceptions altogether; and the if/else ternary (three-part) expression, which is essentially an if statement squeezed onto a single line. Here are a few examples:
>>>value = D.get('x', 0)# Index but with a default >>>value0 >>>value = D['x'] if 'x' in D else 0# if/else expression form >>>value0
We’ll save the details on such alternatives until a later chapter. For now, let’s turn to another dictionary method’s role in a common use case.
As mentioned earlier, because dictionaries are not sequences, they don’t maintain any dependable left-to-right order. If we make a dictionary and print it back, its keys may come back in a different order than that in which we typed them, and may vary per Python version and other variables:
>>>D = {'a': 1, 'b': 2, 'c': 3}>>>D{'a': 1, 'c': 3, 'b': 2}
What do we do, though, if we do need to impose an ordering on a dictionary’s items? One common solution is to grab a list of keys with the dictionary keys method, sort that with the list sort method, and then step through the result with a Python for loop (as for if, be sure to press the Enter key twice after coding the following for loop, and omit the outer parenthesis in the print in Python 2.X):
>>>Ks = list(D.keys())# Unordered keys list >>>Ks# A list in 2.X, "view" in 3.X: use list() ['a', 'c', 'b'] >>>Ks.sort()# Sorted keys list >>>Ks['a', 'b', 'c'] >>>for key in Ks:# Iterate though sorted keysprint(key, '=>', D[key])# <== press Enter twice here (3.X print) a => 1 b => 2 c => 3
This is a three-step process, although, as we’ll see in later chapters, in recent versions of Python it can be done in one step with the newer sorted built-in function. The sorted call returns the result and sorts a variety of object types, in this case sorting dictionary keys automatically:
>>>D{'a': 1, 'c': 3, 'b': 2} >>>for key in sorted(D):print(key, '=>', D[key])a => 1 b => 2 c => 3
Besides showcasing dictionaries, this use case serves to introduce the Python for loop. The for loop is a simple and efficient way to step through all the items in a sequence and run a block of code for each item in turn. A user-defined loop variable (key, here) is used to reference the current item each time through. The net effect in our example is to print the unordered dictionary’s keys and values, in sorted-key order.
The for loop, and its more general colleague the while loop, are the main ways we code repetitive tasks as statements in our scripts. Really, though, the for loop, like its relative the list comprehension introduced earlier, is a sequence operation. It works on any object that is a sequence and, like the list comprehension, even on some things that are not. Here, for example, it is stepping across the characters in a string, printing the uppercase version of each as it goes:
>>>for c in 'spam':print(c.upper())S P A M
Python’s while loop is a more general sort of looping tool; it’s not limited to stepping across sequences, but generally requires more code to do so:
>>>x = 4>>>while x > 0:print('spam!' * x)x -= 1spam!spam!spam!spam! spam!spam!spam! spam!spam! spam!
We’ll discuss looping statements, syntax, and tools in depth later in the book. First, though, I need to confess that this section has not been as forthcoming as it might have been. Really, the for loop, and all its cohorts that step through objects from left to right, are not just sequence operations, they are iterable operations—as the next section describes.
If the last section’s for loop looks like the list comprehension expression introduced earlier, it should: both are really general iteration tools. In fact, both will work on any iterable object that follows the iteration protocol—pervasive ideas in Python that underlie all its iteration tools.
In a nutshell, an object is iterable if it is either a physically stored sequence in memory, or an object that generates one item at a time in the context of an iteration operation—a sort of “virtual” sequence. More formally, both types of objects are considered iterable because they support the iteration protocol—they respond to the iter call with an object that advances in response to next calls and raises an exception when finished producing values.
The generator comprehension expression we saw earlier is such an object: its values aren’t stored in memory all at once, but are produced as requested, usually by iteration tools. Python file objects similarly iterate line by line when used by an iteration tool: file content isn’t in a list, it’s fetched on demand. Both are iterable objects in Python—a category that expands in 3.X to include core tools like range and map.
I’ll have more to say about the iteration protocol later in this book. For now, keep in mind that every Python tool that scans an object from left to right uses the iteration protocol. This is why the sorted call used in the prior section works on the dictionary directly—we don’t have to call the keys method to get a sequence because dictionaries are iterable objects, with a next that returns successive keys.
It may also help you to see that any list comprehension expression, such as this one, which computes the squares of a list of numbers:
>>>squares = [x ** 2 for x in [1, 2, 3, 4, 5]]>>>squares[1, 4, 9, 16, 25]
can always be coded as an equivalent for loop that builds the result list manually by appending as it goes:
>>>squares = []>>>for x in [1, 2, 3, 4, 5]:# This is what a list comprehension doessquares.append(x ** 2)# Both run the iteration protocol internally >>>squares[1, 4, 9, 16, 25]
Both tools leverage the iteration protocol internally and produce the same result. The list comprehension, though, and related functional programming tools like map and filter, will often run faster than a for loop today on some types of code (perhaps even twice as fast)—a property that could matter in your programs for large data sets. Having said that, though, I should point out that performance measures are tricky business in Python because it optimizes so much, and they may vary from release to release.
A major rule of thumb in Python is to code for simplicity and readability first and worry about performance later, after your program is working, and after you’ve proved that there is a genuine performance concern. More often than not, your code will be quick enough as it is. If you do need to tweak code for performance, though, Python includes tools to help you out, including the time and timeit modules for timing the speed of alternatives, and the profile module for isolating bottlenecks.
You’ll find more on these later in this book (see especially Chapter 21’s benchmarking case study) and in the Python manuals. For the sake of this preview, let’s move ahead to the next core data type.
[14] Two application notes here. First, as a preview, the rec record we just created really could be an actual database record, when we employ Python’s object persistence system—an easy way to store native Python objects in simple files or access-by-key databases, which translates objects to and from serial byte streams automatically. We won’t go into details here, but watch for coverage of Python’s pickle and shelve persistence modules in Chapter 9, Chapter 28, Chapter 31, and Chapter 37, where we’ll explore them in the context of files, an OOP use case, classes, and 3.X changes, respectively.
Second, if you are familiar with JSON (JavaScript Object Notation)—an emerging data-interchange format used for databases and network transfers—this example may also look curiously similar, though Python’s support for variables, arbitrary expressions, and changes can make its data structures more general. Python’s json library module supports creating and parsing JSON text, but the translation to Python objects is often trivial. Watch for a JSON example that uses this record in Chapter 9 when we study files. For a larger use case, see MongoDB, which stores data using a language-neutral binary-encoded serialization of JSON-like documents, and its PyMongo interface.