kojiro
kojiro

Reputation: 77137

Type hint where one argument is the type of another

I'm having trouble expressing the precise type hints for a pair of parameters where one needs to be a value that is an instance of some type, and the other needs to be the type itself, or some super-type.

One case where you see this in Python is in the __exit__ method for a context manager (class).

import typing as t
from types import TracebackType
from contextlib import AbstractContextManager

class Acme(AbstractContextManager):
  def __exit__(self, exc_type: t.Type[Exception], exc: Exception,
      tb: Tracebacktype) -> None:
    ...

This specific case may not matter, because context managers are handled internally by Python, but I'd still like to understand how to express two arguments where the first one is the (super-)type of the second one.

Conceptually, my problem is I want to express that the value exc has the type exc_type or some subtype. In the expression above, I guess mypy would be perfectly happy with arguments like LookupError, RuntimeError('foo') even though a RuntimeError isn't a type of LookupError. Is there a better way to express this where mypy would catch that kind of mistake?

Update

Trying a test case here using TypeVars:

import typing as t

C = t.TypeVar('C', bound=t.Collection)

def f(t: t.Type[C], c: C) -> None:
  pass

f(t.Dict, [])

I'd expect mypy to complain about this code, because even though the empty list is a type of Collection, it's not a dictionary.

Upvotes: 4

Views: 1282

Answers (2)

modesitt
modesitt

Reputation: 7210

This kind of use of Type[some_type_var] is seemingly discouraged by a seciton in the PEP where only a subset of this support is provided (notably for binding TypeVars to class types). It appears this discouragement is not phillisophical (this would be a common use of generics in a language where types and values are both first-class) - but more related to the practical implementation of type checkers. The use of TypeVar for classmethod signatures - a special case of your question - was even a late addition to the pep.


Possible solutions that do not work with mypy (as of now):

Standard TypeVar

There is no type checking in function signatures of Type[some_type_var]. This is not variable based on if some_type_var is contravariant or covariant (nor should it be).

TypeVar of Type[TypeVar]

T = TypeVar("T")
Meta = TypeVar("Meta", bound=Type[T])

def foo(the_type: Meta, the_value: T):
   ....

T and Meta can not "bind" together .


To my surprise, in some cases you can squeeze out a bit of this behavior (but this is a special case and this seems undefined to me)

T = TypeVar('T', str, int, float, bytes)

class Foo:

    def bar(self, the_type: Type[T], the_value: T):
        print(isinstance(the_value, the_type))

f = Foo()
f.bar(str, "baz")
f.bar(str, b"baz")  # wrong!
f.bar(int, 1)
f.bar(int, 3.15159)  # wrong! (but no complaint)
f.bar(float, 1.0)
f.bar(float, 1)  # wrong! (but no complaint)
f.bar(float, b'1.0')  # wrong!

Giving

so.py:nn: error: Value of type variable "T" of "bar" of "Foo" cannot be "object"
so.py:nn: error: Value of type variable "T" of "bar" of "Foo" cannot be "object"
Found 2 errors in 1 file (checked 1 source file)

But only seemingly for python's primitive types (this will not work with user-space types) (and again, only some of python's primitive types as shown (see the error-miss with float and int). I think this is a clear "nice-to-have" extension of TypeVar (and makes more Generic use-cases possible).


There are some related issues on mypy about this:

  1. about class annotation behavior
  2. TypeVar of a TypeVar

Upvotes: 3

Samwise
Samwise

Reputation: 71517

This is what TypeVars are for! You want something like:

from types import TracebackType
from typing import Type, TypeVar
from contextlib import AbstractContextManager

_E = TypeVar('_E', bound=Exception)

class Acme(AbstractContextManager):
    def __exit__(
        self,
        exc_type: Type[_E],
        exc: _E,
        tb: Tracebacktype
    ) -> None:
    ...

Upvotes: 0

Related Questions