Handling of a undefined Generic results to `Any`/Unknown

I am struggeling in adding type for a quite simple class.

It is an Interval class, that either represents an open or closed interval (so begin and end are optional).

My requirement is that given I create a new interval and the type checker knows that I created a closed interval, start and end should be not None but my “time” (int here as example).

If an Interval is processed with an method there should be a TypeGuard that tell the typed checker: Hey both values are defined.

So I got this (note the different bound and contrains are only to differentiate the two cases in one example)

from typing import TypeVar, Generic, TypeGuard

TStart = TypeVar("TStart", bound=int|None)  # can be any subtype of int|None
TEnd = TypeVar("TEnd", int, None) # must be either int or None (exactly, no subclass)

class Interval(Generic[TStart, TEnd]):
    def __init__(self, start: TStart, end: TEnd) -> None:
        if start is None and end is None:
            raise ValueError("Interval is not defined")
        self.start: TStart  = start
        self.end: TEnd = end
        
    def do(self) -> None:
        reveal_type(self.start) 
        reveal_type(self.end)  # either int or none
    
    @staticmethod
    def is_closed(me) -> TypeGuard["Interval[int, int]"]:
        return isinstance(me.begin,int) and isinstance(me.end, int)

def foo(i: Interval) -> int:
    reveal_type(i)
    y: int = 0 
    x: int = i.start + i.end  #  ❌ this should produce an error from the type checker
    reveal_type(i.start)  # this should evaluate int|None
    reveal_type(i.end) # maybe this, too ?
    if Interval.is_closed(i):
        reveal_type(i)
        y = i.start + i.end  # this work
    return x + y 
    
     
inter = Interval(1,2)
# This should pass and it does
inter.start-2*inter.end

With this implementation neither pyright nor MyPy will find the marked error.

MyPy evaluated i.start and i.end in Any and pyright into Unkown.
But why isn’t it int | None ?

Do I have to wait on PEP 696, TypeVar defaults in order to annotate this code correctly?

You need to explictly tell the static type checkers that foo is a generic function to stop them from filling in Any (or an alias for it, Unknown) for the type variables:

def foo(i: Interval[TStart, TEnd]) -> int:
   ...

I think then the behavior is as you expect.

1 Like

Thank you for the clarification.

Stilll not what I would expect. Interval is part of an internal library.
The idea of introducing type checkers to our code base is reducing potential errors. If a user now imports Interval and annotates a function they won’t receive type errors even so the code is typed.

That is very contra intuitive.

With more strict type checker settings, I get an error if I just annotate something with Interval (without any type variable).

2 Likes

As others have pointed out, type checkers generally only show errors for this condition when strict settings are enabled. For mypy the flag is --disallow-any-generics; pyright surely has a similar flag but I haven’t looked it up.

Possibly type checkers should turn this check on by default, but that would mean that annotating something as e.g. just list would produce an error, which might be unfriendly for beginners. In any case, that’s a decision for individual type checkers to make.

PEP 696 (TypeVar defaults) would help here in the sense that you can provide some default other than Any. In your example, it might make sense to have both type parameters default to int.

The option in pyright is called reportMissingTypeArgument. It is enabled by default in “strict” type checking mode but disabled by default in “standard” or “basic” modes.

Code sample in pyright playground

Thank you everybody for your help and explanations.

The strict flag is very helpful. In the future this is definitely one of the flag we will enable.

One thing I still don’t understand is why type checkers assume the type var is Any / Unknown instead of simply assuming it is the bound value, so int|None or the restricted value, so int or None (exactly this classes, no subtypes).

I understand that we need this strict mode for unbound TypeVars (as they are similar to Any) but if a TypeVar is bound, why don’t use this bound?

The short answer is “because this is what the spec says”.

It was presumably spec’ed this way to support gradual typing. Assuming Any is more conducive to gradual typing, especially when the type parameter is invariant.