Execute Program

Python in Detail: Dicts Are Fundamental

Welcome to the Dicts Are Fundamental lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • Most modern programming languages have a data structure similar to Python's dict. JavaScript has Map, and basic JavaScript objects also have many similarities to dict. Ruby has hash. Java has HashMap. Rust also has HashMap.

  • The data structure itself is very common! But Python's relationship to dict is different from these other languages in an important way.

  • In all of these languages, dictionaries (or maps or hashes or hashmaps) are a tool to be used as needed. But the entire Python language is built on dictionaries, even including fundamental features like object attributes.

  • We've already seen that the .__dict__ method on objects gives us the dictionary where the object's attributes are stored.

  • >
    class Cat:
    def __init__(self, name, vaccinated):
    self.name = name
    self.vaccinated = vaccinated

    def needs_vaccination(self):
    return not self.vaccinated
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    keanu = Cat("Keanu", True)
    keanu.__dict__
    Result:
  • We can use .__dict__ like a regular dictionary. If we add a key to .__dict__, it shows up as an attribute on the object.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    keanu = Cat("Keanu", True)
    keanu.__dict__["we_added_this_attribute"] = 1
    keanu.we_added_this_attribute
    Result:
    1Pass Icon
  • This also works for class attributes, since classes are just a special kind of object. And it works for methods, since methods are just attributes that are also callable.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    Cat.needs_vaccination is Cat.__dict__['needs_vaccination']
    Result:
    TruePass Icon
  • We can even call methods directly from Cat.__dict__.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    keanu = Cat("Keanu", True)
    # This does the same thing as `keanu.needs_vaccination()`.
    Cat.__dict__['needs_vaccination'](keanu)
    Result:
    FalsePass Icon
  • Most languages let us list an object's attributes, or list an object's methods. But .__dict__ isn't just a way to ask for a list of attributes or methods. It's bidirectional, as we saw when we added a key in the example above: we can change the object by changing its .__dict__. In Python, .__dict__ really is where the attributes are stored. It's not just a trick for our convenience!

  • Dicts show up in some other surprisingly fundamental places. For example, we can call globals() to get a dict of all global variables that are currently in scope.

  • >
    type(globals())
    Result:
    dictPass Icon
  • >
    name = "Amir"
    globals()["name"]
    Result:
    'Amir'Pass Icon
  • The globals() dict allows the same kind of bidirectional manipulation that we saw with .__dict__. We can define a new global variable by adding a key to the globals() dict!

  • >
    globals()["twelve"] = 12
    twelve
    Result:
    12Pass Icon
  • We can get a list of all globals by looking at globals().keys().

  • >
    twelve = 12
    list(globals().keys())
    Result:
  • The assert_raises global is specific to Execute Program. We use it in some code problems to confirm that your code raises the expected exception.

  • The __builtins__ variable is topic we'll cover in another lesson. And twelve is the variable that we created.

  • There are limits to the "everything is a dict" philosophy. The first involves locals(), which is like globals() except that it holds local variables. Unlike globals(), this dict is meant to be read-only. Python will technically let us write to the locals() dict, but that won't actually change the variables' values.

  • >
    def read_local():
    x = 1
    locals()["x"] = 2
    return x

    read_local()
    Result:
    1Pass Icon
  • That exception is an implementation detail of Python. Internally, the Python runtime optimizes functions in many ways, some of which are incompatible with a writable locals() dict.

  • The second exception involves the .__dict__ attribute. For performance reasons, some Python objects don't have a .__dict__ at all. For example, ints, strs, and several other primitive types don't have .__dict__s. Trying to access .__dict__ on them is an error.

  • >
    (5).__dict__
    Result:
    AttributeError: 'int' object has no attribute '__dict__'Pass Icon
  • >
    ("a").__dict__
    Result:
    AttributeError: 'str' object has no attribute '__dict__'Pass Icon
  • Ints, strings, and other fundamental types are very common, so their performance is critical. Omitting a .__dict__ on these types allows additional optimizations.

  • (It's possible to intentionally opt out of .__dict__ in the same way, even for our own classes. If we write a data structure that's very performance-sensitive or memory-sensitive, we can choose not to have a .__dict__ at all. But the full details of that are out of scope here.)

  • As you write more Python code, you'll find dicts in many more places. This makes Python feel more tangible than many other languages. We can see all of the globals just by calling globals(); we can see an object's attributes by looking at .__dict__; etc. This lets us dynamically explore the structure of our objects, classes, functions, and modules. When learning a new codebase, experienced Python programmers often use the Python console (the "REPL") to dig into the runtime values interactively.