10.3. Type Hints

Many programming languages have static typing, meaning that program- mers must declare the data types of all variables, parameters, and return values in the source code. This allows the interpreter or compiler to check that the code uses all objects correctly before the program runs. Python has dynamic typing: variables, parameters, and return values can be of any data type or even change data types while the program runs. Dynamic lan- guages are often easier to program with because they require less formal specification, but they lack the bug-preventing advantages that static lan- guages have. If you write a line of Python code, such as round(‘forty two’) , you might not realize that you’re passing a string to a function that accepts only int or float arguments until you run the code and it causes an error. A statically typed language gives you an early warning when you assign a value or pass an argument of the wrong type.

Python’s type hints offer optional static typing. In the following example, the type hints are in bold:

>>> def describeNumber(number: int) -> str:
>>>     if number % 2 == 1:
>>>         return 'An odd number. '
>>>     elif number == 42:
>>>         return 'The answer. '
>>>     else:
>>>         return 'Yes, that is a number. '
>>> myLuckyNumber: int = 42
>>> print(describeNumber(myLuckyNumber))
The answer.

As you can see, for parameters or variables, the type hint uses a colon to separate the name from the type, whereas for return values, the type hint uses an arrow ( -> ) to separate the def statement’s closing parentheses from the type. The describeNumber() function’s type hints show that it takes an integer value for its number parameter and returns a string value.

If you use type hints, you don’t have to apply them to every bit of data in your program. Instead, you could use a gradual typing approach, which is a compromise between the flexibility of dynamic typing and the safety of static typing in which you include type hints for only certain variables, parameters, and return values. But the more type hinted your program is, the more information the static code analysis tool has to spot potential bugs in your program.

Notice in the preceding example that the names of the specified types match the names of the int() and str() constructor functions. In Python, class, type, and data type have the same meaning. For any instances made from classes, you should use the class name as the type:

import datetime noon: datetime.time = datetime.time(12, 0, 0) class CatTail: def init(self, length: int, color: str) -> None: self.length = length self.color = color zophieTail: CatTail = CatTail(29, ‘grey’)

The noon variable has the type hint datetime.time 1 because it’s a time object (which is defined in the datetime module). Likewise, the zophieTail object has the CatTail type hint 2 because it’s an object of the CatTail class we created with a class statement. Type hints automatically apply to all sub- classes of the specified type. For example, a variable with the type hint dict could be set to any dictionary value but also to any collections.OrderedDict and collections.defaultdict values, because these classes are subclasses of dict . Chapter 16 covers subclasses in more detail.

Static type-checking tools don’t necessarily need type hints for vari- ables. The reason is that static type-checking tools do type inference, infer- ring the type from the variable’s first assignment statement. For example, from the line spam = 42 , the type checker can infer that spam is supposed to have a type hint of int . But I recommend setting a type hint anyway. A future change to a float , as in spam = 42.0 , would also change the inferred type, which might not be your intention. It’s better to force the programmer to change the type hint when changing the value to confirm that they’ve made an intentional rather than incidental change. ## Using Static Analyzers Although Python supports syntax for type hints, the Python interpreter completely ignores them. If you run a Python program that passes an inval- idly typed variable to a function, Python will behave as though the type hints don’t exist. In other words, type hints don’t cause the Python inter- preter to do any runtime type checking. They exist only for the benefit of static type-checking tools, which analyze the code before the program runs, not while the program is running.

We call these tools static analysis tools because they analyze the source code before the program runs, whereas runtime analysis or dynamic analy- sis tools analyze running programs. (Confusingly, static and dynamic in this case refer to whether the program is running, but static typing and dynamic typing refer to how we declare the data types of variables and functions. Python is a dynamically typed language that has static analysis tools, such as Mypy, written for it.) ## Installing and Running Mypy Although Python doesn’t have an official type-checker tool, Mypy is cur- rently the most popular third-party type checker. You can install Mypy with pip by running this command:

python –m pip install –user mypy

Run python3 instead of python on macOS and Linux. Other well-known type checkers include Microsoft’s Pyright, Facebook’s Pyre, and Google’s Pytype.

To run the type checker, open a Command Prompt or Terminal win- dow and run the python –m mypy command (to run the module as an applica- tion), passing it the filename of the Python code to check. In this example, I’m checking the code for an example program I created in a file named example.py:

C:UsersAlDesktop>python –m mypy example.py Incompatible types in assignment (expression has type "float", variable has type "int") Found 1 error in 1 file (checked 1 source file)

The type checker outputs nothing if there are no problems and prints error messages if there are. In this example.py file, there’s a problem on line 171, because a variable named spam has a type hint of int but is being assigned a float value. This could possibly cause a failure and should be investi- gated. Some error messages might be hard to understand at first reading. Mypy can report a large number of possible errors, too many to list here. The easiest way to find out what the error means is to search for it on the web. In this case, you might search for something like “Mypy incompatible types in assignment.”

Running Mypy from the command line every time you change your code is rather inefficient. To make better use of a type checker, you’ll need to configure your IDE or text editor to run it in the background. This way, the editor will constantly run Mypy as you type your code and then display any errors in the editor. Figure 11-1 shows the error from the previous example in the Sublime Text text editor.

_images/1.png

Figure 11-1: The Sublime Text text editor displaying errors from Mypy

The steps to configure your IDE or text editor to work with Mypy differ depending on which IDE or text editor you’re using. You can find instruc- tions online by searching for “<your IDE> Mypy configure,” “<your IDE> type hints setup,” or something similar. If all else fails, you can always run Mypy from the Command Prompt or Terminal window. ## Telling Mypy to Ignore Code You might write code that for whatever reason you don’t want to receive type hint warnings about. To the static analysis tool, the line might appear to use the incorrect type, but it’s actually fine when the program runs. You can suppress any type hint warnings by adding a # type: ignore comment to the end of the line. Here is an example:

>>> def removeThreesAndFives(number: int) -> int:
>>>     number = str(number) # type: ignore
>>>     number = number.replace('3', '').replace('5', '')
>>>     return int(number)
>>> # type: ignore

To remove all the 3 and 5 digits from the integer passed to removeThrees­ AndFives() , we temporarily set the integer number variable to a string. This causes the type checker to warn us about the first two lines in the function, so we add the # type: ignore type hints to these lines to suppress the type checker’s warnings.

Use # type: ignore sparingly. Ignoring warnings from the type checker provides an opening for bugs to sneak into your code. You can almost cer- tainly rewrite your code so the warnings don’t occur. For example, if we create a new variable with numberAsStr = str(number) or replace all three lines with a single return int(str(number.replace(‘3’, ’‘).replace(’5’, ’’))) line of code, we can avoid reusing the number variable for multiple types. We wouldn’t want to suppress the warning by changing the type hint for the parameter to Union[int, str] , because the parameter is meant to allow integers only. ## Setting Type Hints for Multiple Types Python’s variables, parameters, and return values can have multiple data types. To accommodate this, you can specify type hints with multiple types by importing Union from the built-in typing module. Specify a range of types inside square brackets following the Union class name:

>>> from typing import Union
>>> spam: Union[int, str, float] = 42
>>> spam = 'hello'
>>> spam = 3.14

In this example, the Union[int, str, float] type hint specifies that you can set spam to an integer, string, or floating-point number. Note that it’s preferable to use the from typing import X form of the import statement rather than the import typing form and then consistently use the verbose typing.X for type hints throughout your program.

You might specify multiple data types in situations where a variable or return value could have the None value in addition to another type. To include NoneType , which is the type of the None value, in the type hint, place None inside the square brackets rather than NoneType . (Technically, NoneType isn’t a built-in identifier the way int or str is.)

Better yet, instead of using, say, Union[str, None] , you can import Optional from the typing module and use Optional[str] . This type hint means that the function or method could return None rather than a value of the expected type. Here’s an example:

>>> from typing import Optional
>>> lastName: Optional[str] = None
>>> lastName = 'Sweigart'

In this example, you could set the lastName variable to None or a str value. But it’s best to make sparing use of Union and Optional . The fewer types your variables and functions allow, the simpler your code will be, and simple code is less bug prone than complicated code. Remember the Zen of Python maxim that simple is better than complex. For functions that return None to indicate an error, consider raising an exception instead. See “Raising Exceptions vs. Returning Error Codes” on page 178.

You can use the Any type hint (also from the typing module) to specify that a variable, parameter, or return value can be of any data type:

>>> from typing import Any
>>> import datetime
>>> spam: Any = 42
>>> spam = datetime.date.today()
>>> spam = True

In this example, the Any type hint allows you to set the spam variable to a value of any data type, such as int , datetime.date , or bool . You can also use object as the type hint, because this is the base class for all data types in Python. But Any is a more readily understandable type hint than object . As you should with Union and Optional , use Any sparingly. If you set all of your variables, parameters, and return values to the Any type hint, you’d lose the type-checking benefits of static typing. The difference between specify- ing the Any type hint and specifying no type hint is that Any explicitly states that the variable or function accepts values of any type, whereas an absent type hint indicates that the variable or function has yet to be type hinted. ## Setting Type Hints for Lists, Dictionaries, and More Lists, dictionaries, tuples, sets, and other container data types can hold other values. If you specify list as the type hint for a variable, that variable must contain a list, but the list could contain values of any type. The follow- ing code won’t cause any complaints from a type checker:

>>> spam: list = [42, 'hello', 3.14, True]

To specifically declare the data types of the values inside the list, you must use the typing module’s List type hint. Note that List has a capital L, distinguishing it from the list data type:

>>> from typing import List, Union
>>> catNames: List[str] = ['Zophie', 'Simon', 'Pooka', 'Theodore']
>>> numbers: List[Union[int, float]] = [42, 3.14, 99.9, 86]

In this example, the catNames variable contains a list of strings, so after importing List from the typing module, we set the type hint to List[str] 1. The type checker catches any call to the append() or insert() method, or any other code that puts a nonstring value into the list. If the list should contain multiple types, we can set the type hint using Union . For example, the numbers list can contain integer and float values, so we set its type hint to List[Union[int, float]] 2.

The typing module has a separate type alias for each container type. Here’s a list of the type aliases for common container types in Python:

List is for the list data type.

Tuple is for the tuple data type.

Dict is for the dictionary ( dict ) data type.

Set is for the set data type.

FrozenSet is for the frozenset data type.

Sequence is for the list , tuple , and any other sequence data type.

Mapping is for the dictionary ( dict ), set , frozenset , and any other mapping data type.

ByteString is for the bytes , bytearray , and memoryview types.

You’ll find the full list of these types online at https://docs.python.org/3/ library/typing.html#classes-functions-and-decorators. ## Backporting Type Hints with Comments Backporting is the process of taking features from a new version of soft- ware and porting (that is, adapting and adding) them to an earlier version. Python’s type hints feature is new to version 3.5. But in Python code that might be run by interpreter versions earlier than 3.5, you can still use type hints by putting the type information in comments. For variables, use an inline comment after the assignment statement. For functions and meth- ods, write the type hint on the line following the def statement. Begin the comment with type: , followed by the data type. Here’s an example of some code with type hints in the comments:

>>> from typing import List
>>> spam = 42 # type: int
>>> def sayHello():
>>> # type: () -> None
>>>     """The docstring comes after the type hint comment."""
>>>     print('Hello!')
>>> def addTwoNumbers(listOfNumbers, doubleTheSum):
>>> # type: (List[float], bool) -> float
>>>     total = listOfNumbers[0] + listOfNumbers[1]
>>>     if doubleTheSum:
>>>         total *= 2
>>>     return total

Note that even if you’re using the comment type hint style, you still need to import the typing module 1, as well as any type aliases that you use in the comments. Versions earlier than 3.5 won’t have a typing module in their standard library, so you must install typing separately by running this command:

python –m pip install --user typing

Run python3 instead of python on macOS and Linux. To set the spam variable to an integer, we add # type: int as the end-of-line comment 2. For functions, the comment should include parentheses with a comma-separated list of type hints in the same order as the parameters. Functions with zero parameters would have an empty set of parentheses 3. If there are multiple parameters, separate them inside the parentheses with commas 4.

The comment type hint style is a bit less readable than the normal style, so use it only for code that might be run by versions of Python earlier than 3.5.