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.
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.