Asserting a variable has certain properties if it passes certain tests

Viewed 161

Using the python typing system, I sometimes know the type of a variable or an expression because I have installed runtime checking to verify it. Is there a way to denote this with annotations?

For example, suppose I have a function combop which returns True if its argument is of class Combination. And further suppose that the class Combination has a field called tds.

if combop(x):
   return f(x.tds)
else:
   return f([])

In this example, I'd like to promise the checker that within the then part of the if, that x has type Combination, and x.tds makes sense.

An example of such a situation is line 172 of here python-rte. The IDE complains about d.tds and also n.s. I am supposing that there is a way to explain my intentions with type annotations, and the IDE would accept or verify my claim if possible.

The way this is done in some functional languages (e.g. Scala) is with pattern matching. Within a clause of a pattern match the type inferencer is able to restrict the type of certain variables. In some other languages (e.g. Common Lisp) there is a declare syntax which allows the programmer to give hints to the type inferencer which may or may not contribute a run-time overhead, depending on compiler settings.

2 Answers

Python 3.10 will introduce the new TypeGuard feature, which might provide you with what you're looking for. (The feature is also available through the typing_extensions module for Python <= 3.9.) A TypeGuard allows you to assert to the type-checker that an object is of a certain type. So, you could write a function like this:

from typing import TypeGuard, Any

def combop(obj: Any) -> TypeGuard[Combination]:
    # Some code that returns `True` if the object is of type `Combination`, else `False`.

If you're more interested in the properties of your object (the "structural type" as opposed to the "nominal type" of your object), then you could use a TypeGuard to assert that your object conforms to a certain protocol.

from typing import Protocol, TypeGuard, Any

class CombinationsProto(Protocol):
    tds: list[Any]


def combops(obj: Any) -> TypeGuard[CombinationsProto]:
    # Some code here that returns `True` if the object has a `tds` attribute, else `False`.

As this is a new feature in the python-typing world, Python's major type-checkers are still working on support for this feature, especially with respect to some complex corner cases.

import typing


class Foo:
    def __init__(self) -> None:
        self.bar = "zebra"  # will be of 'str' type


def give_me_something():  # no annotation here, can't tell what gets returned
    if dict():  # an empty dict is always False, this is just an example, a real condition could be used
        return []
    else:
        return Foo()


reveal_type(give_me_something())
reveal_type(typing.cast(Foo, give_me_something()))
$ mypy so69205255.py
so69205255.py:16: note: Revealed type is "Any"
so69205255.py:17: note: Revealed type is "so69205255.Foo"

I used MyPy to reproducibly check, but my IDE PyCharm also understands it : when I hover the variable name I assigned the expression to, it says that the first one is of type Union[list, Foo] while the second is of type Foo.

More info : see the docs

Related