Saturday 18 February 2023

Python Type Hints, Cast and Any

When I came back into Python programming 1 year ago and came across ype hints I was not particularly excited about them. I had the same feeling I'd had when TypeScript arrived to the JavaScript world, that it was somehow limiting the power of an almighty dynamic language, and started to use them occasionally, but now I'm totally hooked to type hints. I use them even for the most trivial scripts. The sort of "basic documentation" that type annotations provide, and the way they empower intellisense and prevent bugs is something that seems essential to me now. The amazing thing is that using Any, cast and Protocols you can have the best of 2 worlds, ultra dynamic, duck-typed code with object expansion, inheritance modification and so on, and the development friendliness provided by type hints.

Any is what in this post I qualified as Top and Bottom type. Objects of any type can be assigned to a variable hinted as Any, and an object hinted as Any can be assigned to a variable of whatever type.
We can use Any in sections of code where we don't care about type checking. Assign the Any type to a variable or parameter and the type-checker will not complain about anything that we try to do with that object.

Casting. The typing.cast function in python behaves like the rest of the type hints infrastructure, it's used only by they type checker (mypy for example) and has no effect at runtime (we know that the only effect that type hints have at runtime is that they are available as annotations). While in static typed languages like Java and C# casting an object to an incorrect type will cause a runtime error, in python it'll do nothing, as cast() just returns the instance object passed to it. Duck-typing is one of those Python features that we love so much, and we can make it play nicely with type hints just by using a cast. My object is not a Duck, but it quacks, and my function expects a Duck, but just to make it quack, so let's pretend to be a Duck by casting to a Duck, so that the function accepts it.


lass Animal:
    def __init__(self, name):
        self.name = name

    def move(self):
        print(f"Animal {self.name} is moving")

class Button:
    def __init__(self, name):
        self.name = name

    def move(self):
        print(f"Button {self.name} is moving")

def do_sport(item: Animal):
    print("doing sport")
    item.move()

a1 = Animal("Xana")
do_sport(a1)

b1 = Button("Slide")
# inferred type is Button

# at runtime this is fine thanks to Duck Typing
# but the type-checker complains:
# error: Argument 1 to "do_sport" has incompatible type "Button"; expected "Animal"
do_sport(b1)

# so let's cast it
do_sport(cast(Animal, b1))

# if we cast it to Any we get the same effect, but obviously we lose intellisense
do_sport(cast(Any, b1))

We have an interesting combined use of casting to Any and casting to an specific type when we are expanding an object (adding to it a method for example). Let's see it:


from typing import cast, Any
import types 

class Animal:
    def __init__(self, name):
        self.name = name

    def move(self):
        print(f"{self.name} is moving")

class Person:
    def __init__(self, name):
        self.name = name

    def speak(self):
        print(f"{self.name} is speaking")

p1 = Person("Francois")
# let's add to this instance (expand it) the move method from the Animal class. We have to create a bound method, which we do with types.MethodType

# this works fine at runtime:
p1.move = types.MethodType(Animal.move, p1)
p1.move()
#but the type-checker complains
#expando.py:21: error: "Person" has no attribute "move"  [attr-defined]
#expando.py:22: error: "Person" has no attribute "move"  [attr-defined]


# we cast to Any to prevent this type-checker error: Cannot assign to a method  [assignment]
(cast(Any, p1)).move = types.MethodType(Animal.move, p1)
# and now we cast it to Animal and assign to a new variable so that we can use it comfortably
p2 = cast(Animal, p1)
p2.move()

No comments:

Post a Comment