Locked learning resources

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

Unlock This Lesson

Locked learning resources

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

Unlock This Lesson

Typing: ReadOnly and TypeIs

00:00 In the previous lesson, I showed you the new deprecation decorator and default values for template types. In this lesson, I’ll continue with the additions to the typing system.

00:09 First, talking about read-only TypedDict attributes and then about the TypeIs guard. The TypedDict is a data class-like thing where you can declare the types of the values for specific keys in a dictionary.

00:22 The typing system allows you to annotate the keys. For example, you can specify whether a value is required or not. Python 3.13 has added a read-only typing annotation.

00:34 It’s important to note that this is only a typing annotation. It doesn’t actually change the behavior of Python. Let’s go take a look at it.

00:44 In the top window, I have a TypedDict class with an attribute called version. I’ve annotated that attribute as a read-only string.

00:53 If I attempt to mutate the value like I have on line eight, the type checker will scream. Again, to be clear, it’s only a type annotation.

01:05 Python still does Python, it doesn’t stop me from changing it. But when I run the type checker,

01:15 it tells me that the version attribute is meant to be immutable.

01:21 There are situations in typing where a function might return multiple different types. A type guard is a piece of code that narrows down the possible results for the type checker.

01:31 For simple cases, like when using an is None comparison, this can be done directly from the code, but for more complicated cases, you need to help the type checker out.

01:42 You can do this by writing your own TypeGuard. A TypeGuard is a function that returns a Boolean based on typing information. True if it is the type and false if it isn’t. Python 3.13 has introduced TypeIs which does something similar to TypeGuard but is more restrictive.

02:00 This is actually a good thing as what you typically want to do is narrow the type down as much as possible. They even considered changing the behavior of the TypeGuard declaration, but were worried about backwards compatibility.

02:12 Unless you really need a TypeGuard, you should now prefer TypeIs instead. Let’s go look at the difference in some code.

02:21 Consider a tree made up of nested lists of lists where the leaves are integers. Here I have a function that takes a tree or a subset of the tree and either descends until it finds a leaf and returns that, or if the subset is a leaf itself, just returns the leaf.

02:38 The argument to this function is thus either a tree or an integer and I tell the type checker that by using the Tree | int annotation.

02:49 Narrowing down the type of the return value is too complex for the regular mechanisms, so here I’m using a TypeGuard function that returns true if the argument is a tree.

03:00 And this is the implementation of that TypeGuard function. It specifies that it’s a type guard by using the TypeGuard annotation.

03:08 If it was a tree, I recursively call the function and descend. If it wasn’t a tree, it’s a leaf and I should just return it. And this is where the problem is.

03:19 The call to the TypeGuard on line 10 ensures that the type gets narrowed down within the if statement, but past the if statement, the typing information gets lost.

03:29 So this final return can still be either a tree or an int even though you and I know it has to be an int because it got passed the previous if statement.

03:40 Rather than fix this behavior, there are certain situations where you actually might want it. Python 3.13 has introduced a new kind of guard, the TypeIs annotation.

03:51 This alternate version of get_leaf_value() has the same declaration as above and like before has an if statement that checks the type.

04:00 Here’s my new guard though, this time it uses TypeIs instead of TypeGuard.

04:06 Now, down here at the bottom, because of the way TypeIs has been implemented, it is known that this has to be an integer. Guards take a little to wrap your head around and I’ll admit every time I think I understand them, that knowledge disappears within a day or two.

04:20 To show the problem in action, I’m going to run this script through Pyright in 3.13 mode. What should happen is a failure on line 13 because the type guard on line 10 fails to narrow down the type sufficiently.

04:34 The get_left_leaf_value_312() function has been annotated to only return an int, but because of the use of TypeGuard rather than TypeIs, the return value on line 13 is considered either a tree or an int, so it’ll fail the type check.

04:54 Calling Pyright

05:02 and there it is. Line 13 can be a tree or an int, whereas the function said it should only be an int and seeing as TypeIs does narrow it down more, the only error is in the 3.12 code.

05:15 The 3.13 code passes fine.

05:20 The next lesson has two smaller improvements. The first is command line use of the random module, and the second is small changes to docstrings.

Become a Member to join the conversation.