Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

This lesson is for members only. Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Hint: You can adjust the default video playback speed in your account settings.
Hint: You can set your subtitle preferences in your account settings.
Sorry! Looks like there’s an issue with video playback 🙁 This might be due to a temporary outage or because of a configuration issue with your browser. Please refer to our video player troubleshooting guide for assistance.

Type Hinting Improvements

00:00 In the previous lesson, I introduced you to the new pattern matching syntax in Python 3.10. In this lesson, I’ll be talking about the improvements to type hinting.

00:10 Several releases ago, Python introduced the ability to annotate your code with metadata. At the time, the focus was on the annotations more than anything else. Very quickly, the most common use of annotations was to provide type hinting to the language.

00:24 Python is a dynamically typed language, which means there is very little that the compiler can do to detect the incorrect use of types. Python doesn’t let you add a string and an integer, but it has no way of knowing you’re attempting to do that until it actually executes the line of code.

00:40 Type hints are a way of introducing static typing style features into the otherwise dynamic language. You annotate your code with information about the allowed types for a given situation, which means tools can determine what type a function’s arguments expect, what type a function returns, or what type a variable contains.

01:02 Third-party tools like mypy and many IDEs can take advantage of this type hint information and inform you of potential errors before you even run your code.

01:12 Type hinting features have grown steadily over the last several releases, and Python 3.10 is no different.

01:19 There are situations where you want to support multiple types. A common example of this is a function that can take any number, whether int or float, as an argument. To specify a choice of types, Python has the Union type that allows the joining of several different types.

01:36 The problem with this is it can get unwieldy and long. The sum_list() function here in the example takes a list of numbers. The resulting type hint requires two special imports and takes up a lot of space. Python 3.10 introduces a shortcut for the Union type, the pipe or OR operator. Combining this with the Python 3.9 addition of being able to use the list itself as a type, and you no longer need the import and the type hint itself is shorter and easier to read.

02:07 The same operator can also be used to specify that a value is optional. This code shows a type hint for a variable named address that can be a string or None. Prior to Python 3.10, you would have had to import and use the Optional type hint class to do the same thing.

02:25 You can also take advantage of the union type hint operator at runtime, allowing the creation of OR conditions on type checking inside of a call to isinstance().

02:35 The example here returns True if user_id is either a string or an integer.

02:43 Python supports the creation of more complex type hints through the use of assignment. Python 3.10 introduces the TypeAlias class to make this more explicit.

02:53 The example here declares a Card to be a tuple made up of two strings and a Deck as a list of cards. This could be done prior to Python 3.10, but it was done in a way where there was ambiguity for the parser.

03:06 You could have been declaring a type hint or a global variable. The TypeAlias class makes this much more clear.

03:15 A static type checker that is presented with values that can have multiple options may be able to reduce the list of options by examining the code. This is called type narrowing.

03:26 The get_ace() function call has a type hint saying that the suit argument can either be a string or None. The if condition block is checking if the suit is None.

03:38 This is called a type guard. A clever static type checker now knows that past this point, the suit has to be a string. So, the first part of the tuple in the return type of this function is always a string. Without the type guard, this function would fail its type check.

03:56 The function says it returns a tuple of strings, but if the guard wasn’t there, the first part of the tuple would have been a string or None.

04:06 There’s a limited list of the kinds of code that a type checker can use as a type guard. Python 3.10 introduces the TypeGuard class, allowing you to declare that a function should act as a type guard.

04:18 Your custom function should return True for the guard to apply and False otherwise. In this example code, the is_deck_of_cards() function is declared as a custom TypeGuard.

04:32 Now, when used inside of get_score(), your type checker will treat this function as a type guard and modify its checking algorithm appropriately. Function decorators are closures that wrap a function. They’re useful for applying pre- and post-conditions when the wrapped function is called.

04:51 Because decorators are functions that wrap functions, they hide the type hint information of the thing they are wrapping. This makes type hinting a decorator limiting. In the code here, the callable class indicates that the decorator is wrapping a function.

05:07 It says that it takes a function with some unknown number of arguments, but that’s all you get. To get around this problem, Python 3.10 introduces the ParamSpec class.

05:18 This is a special class for type hinting that acts as a pass-through. The example code still indicates that the decorator takes a Callable, but this time, the function being wrapped is hinted using the ParamSpec.

05:32 This tells the type hinter to pass through the type hints of whatever’s being wrapped. Now, the type information of the decorated function is exposed to the checker.

05:44 The inspect module provides tools for runtime introspection of your code. Python 3.10 has introduced a new function in this module called get_annotations().

05:54 Calling get_annotations() will return all the annotation information associated with the argument. In this example, you see the type hint information associated with the arithmetic mean() function.

06:05 This allows you to introspect not only your code but the annotation of your code at runtime.

06:13 All annotations, type hints or otherwise, must be valid Python code. That might seem obvious, but it has a consequence. Type hints have to be declared before they are used.

06:24 This can make proper type hinting of a recursive class difficult. Python 3.7 introduced the idea of postponed evaluation of annotations, meaning you could annotate with names that hadn’t been declared yet.

06:38 Originally, this was supposed to become the default in Python 3.10 but after conversations with the Python community, several corner cases came up for some popular libraries.

06:48 The decision to implement postponed evaluation was itself postponed until Python 3.11 to give the library maintainers time to catch up. In the meantime, if you want to take advantage of this feature, you can import it from __future__.

07:05 That’s all the type hint goodness. Next up, I’ll talk about asynchronous iteration.

Bartosz Wilk on Aug. 28, 2022

With code:

Card: TypeAlias = tuple[str, str]
Deck: TypeAlias = list[Card]

def is_deck_of_cards(obj: Any) -> TypeGuard[Deck]:
    return isinstance(obj, Deck)

I get an error:

  File "/Volumes/p310_features/type_hints.py", line 34, in is_deck_of_cards
    return isinstance(obj, Deck)
TypeError: isinstance() argument 2 cannot be a parameterized generic

Process finished with exit code 1

Used: Python 3.10, macOS 12.5.1

Christopher Trudeau RP Team on Aug. 28, 2022

Hi Bartosz,

You’re right. This is what happens when you write code in slides, you get lazy and forget to check if it compiles.

This piece of code came out of the companion article, found here:

realpython.com/python310-new-features/#type-unions-aliases-and-guards

The article doesn’t explicitly show the checking but just shows a comment. In this case, to check it you can isinstance(obj, list), but then would need to loop through the whole list to check that each item inside of it to meets the Card definition.

I’ll get the slides updated shortly. Thanks for catching this for me.

Become a Member to join the conversation.