Introducing type hints
Before we can look closely at creating classes, we need to talk a little bit about what a class is and how we're sure we're using it correctly. The central idea here is that everything in Python is an object.
When we write literal values like "Hello, world!"
or 42
, we're actually creating instances of built-in classes. We can fire up interactive Python and use the built-in type()
function on the class that defines the properties of these objects:
>>> type("Hello, world!")
<class 'str'>
>>> type(42)
<class 'int'>
The point of object-oriented programming is to solve a problem via the interactions of objects. When we write 6*7
, the multiplication of the two objects is handled by a method of the built-in int
class. For more complex behaviors, we'll often need to write unique, new classes.
Here are the first two core rules of how Python objects work:
- Everything in Python is an object
- Every object is defined by being an instance of at least one class
These rules have many interesting consequences. A class definition we write, using the class
statement, creates a new object of class type
. When we create an instance of a class, the class object will be used to create and initialize the instance object.
What's the distinction between class and type? The class
statement lets us define new types. Because the class
statement is what we use, we'll call them classes throughout the text. See Python objects, types, classes, and instances - a glossary by Eli Bendersky: https://eli.thegreenplace.net/2012/03/30/python-objects-types-classes-and-instances-a-glossary for this useful quote:
"The terms "class" and "type" are an example of two names referring to the same concept."
We'll follow common usage and call the annotations type hints.
There's another important rule:
- A variable is a reference to an object. Think of a yellow sticky note with a name scrawled on it, slapped on a thing.
This doesn't seem too earth-shattering but it's actually pretty cool. It means the type information – what an object is – is defined by the class(es) associated with the object. This type information is not attached to the variable in any way. This leads to code like the following being valid but very confusing Python:
>>> a_string_variable = "Hello, world!"
>>> type(a_string_variable)
<class 'str'>
>>> a_string_variable = 42
>>> type(a_string_variable)
<class 'int'>
We created an object using a built-in class, str
. We assigned a long name, a_string_variable
, to the object. Then, we created an object using a different built-in class, int
. We assigned this object the same name. (The previous string object has no more references and ceases to exist.)
Here are the two steps, shown side by side, showing how the variable is moved from object to object:
Figure 2.1: Variable names and objects
The various properties are part of the object, not the variable. When we check the type of a variable with type()
, we see the type of the object the variable currently references. The variable doesn't have a type of its own; it's nothing more than a name. Similarly, asking for the id()
of a variable shows the ID of the object the variable refers to. So obviously, the name a_string_variable
is a bit misleading if we assign the name to an integer object.
Type checking
Let's push the relationship between object and type a step further, and look at some more consequences of these rules. Here's a function definition:
>>> def odd(n):
... return n % 2 != 0
>>> odd(3)
True
>>> odd(4)
False
This function does a little computation on a parameter variable, n
. It computes the remainder after division, the modulo. If we divide an odd number by two, we'll have one left over. If we divide an even number by two, we'll have zero left over. This function returns a true value for all odd numbers.
What happens when we fail to provide a number? Well, let's just try it and see (a common way to learn Python!). Entering code at the interactive prompt, we'll get something like this:
>>> odd("Hello, world!")
Traceback (most recent call last):
File "<doctestexamples.md[9]>", line 1, in <module>
odd("Hello, world!")
File "<doctestexamples.md[6]>", line 2, in odd
return n % 2 != 0
TypeError: not all arguments converted during string formatting
This is an important consequence of Python's super-flexible rules: nothing prevents us from doing something silly that may raise an exception. This is an important tip:
Python doesn't prevent us from attempting to use non-existent methods of objects.
In our example, the %
operator provided by the str
class doesn't work the same way as the %
operator provided by the int
class, raising an exception. For strings, the %
operator isn't used very often, but it does interpolation: "a=%d" % 113
computes a string 'a=113'
; if there's no format specification like %d
on the left side, the exception is a TypeError
. For integers, it's the remainder in division: 355 % 113
returns an integer, 16
.
This flexibility reflects an explicit trade-off favoring ease of use over sophisticated prevention of potential problems. This allows a person to use a variable name with little mental overhead.
Python's internal operators check that operands meet the requirements of the operator. The function definition we wrote, however, does not include any runtime type checking. Nor do we want to add code for runtime type checking. Instead, we use tools to examine code as part of testing. We can provide annotations, called type hints, and use tools to examine our code for consistency among the type hints.
First, we'll look at the annotations. In a few contexts, we can follow a variable name with a colon, :
, and a type name. We can do this in the parameters to functions (and methods). We can also do this in assignment statements. Further, we can also add ->
syntax to a function (or a class method) definition to explain the expected return type.
Here's how type hints look:
>>> def odd(n: int) -> bool:
... return n % 2 != 0
We've added two type hints to our odd()
little function definition. We've specified that argument values for the n
parameter should be integers. We've also specified that the result will be one of the two values of the Boolean type.
While the hints consume some storage, they have no runtime impact. Python politely ignores these hints; this means they're optional. People reading your code, however, will be more than delighted to see them. They are a great way to inform the reader of your intent. You can omit them while you're learning, but you'll love them when you go back to expand something you wrote earlier.
The mypy tool is commonly used to check the hints for consistency. It's not built into Python, and requires a separate download and install. We'll talk about virtual environments and installation of tools later in this chapter, in the Third-party libraries section. For now, you can use python -m pip install mypy
or conda install mypy
if you're using the conda tool.
Let's say we had a file, bad_hints.py
, in a src
directory, with these two functions and a few lines to call the main()
function:
def odd(n: int) -> bool:
return n % 2 != 0
def main():
print(odd("Hello, world!"))
if __name__ == "__main__":
main()
When we run the mypy
command at the OS's terminal prompt:
% mypy –strict src/bad_hints.py
The mypy tool is going to spot a bunch of potential problems, including at least these:
src/bad_hints.py:12: error: Function is missing a return type annotation
src/bad_hints.py:12: note: Use "-> None" if function does not return a value
src/bad_hints.py:13: error: Argument 1 to "odd" has incompatible type "str"; expected "int"
The def main():
statement is on line 12 of our example because our file has a pile of comments not shown above. For your version, the error might be on line 1. Here are the two problems:
- The
main()
function doesn't have a return type; mypy suggests including-> None
to make the absence of a return value perfectly explicit. - More important is line 13: the code will try to evaluate the
odd()
function using astr
value. This doesn't match the type hint forodd()
and indicates another possible error.
Most of the examples in this book will have type hints. We think they're always helpful, especially in a pedagogical context, even though they're optional. Because most of Python is generic with respect to type, there are a few cases where Python behavior is difficult to describe via a succinct, expressive hint. We'll steer clear of these edge cases in this book.
Python Enhancement Proposal (PEP) 585 covers some new language features to make type hints a bit simpler. We've used mypy version 0.812 to test all of the examples in this book. Any older version will encounter problems with some of the newer syntax and annotation techniques.
Now that we've talked about how parameters and attributes are described with type hints, let's actually build some classes.