Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typing __exit__ in 3.5 fails on runtime, but typechecks

What is the proper type signature for __exit__? I have the following:

from types import TracebackType
from typing import Optional, Type

class Foo:
    def __enter__(self) -> 'Foo':
        return self

    def __exit__(self, exc_type: Optional[Type[BaseException]],
                 exc_value: Optional[BaseException],
                 traceback: Optional[TracebackType]) -> bool:
        return False

On recent mypy (0.560) this typechecks with --strict (and I have moderate faith in this signature, because I stole it from the innards of the typeshed).

When this script is run with python 3.6, as expected nothing happens. But when run with 3.5.2, we get an exception:

Traceback (most recent call last):
  File "/home/student/mypy_test/test.py", line 4, in <module>
    class Foo: #(ContextManager['Foo']):
  File "/home/student/mypy_test/test.py", line 8, in Foo
    def __exit__(self, exc_type: Optional[Type[BaseException]],
  File "/usr/lib/python3.5/typing.py", line 649, in __getitem__
    return Union[arg, type(None)]
  File "/usr/lib/python3.5/typing.py", line 552, in __getitem__
    dict(self.__dict__), parameters, _root=True)
  File "/usr/lib/python3.5/typing.py", line 512, in __new__
    for t2 in all_params - {t1} if not isinstance(t2, TypeVar)):
  File "/usr/lib/python3.5/typing.py", line 512, in <genexpr>
    for t2 in all_params - {t1} if not isinstance(t2, TypeVar)):
  File "/usr/lib/python3.5/typing.py", line 1077, in __subclasscheck__
    if super().__subclasscheck__(cls):
  File "/home/student/.local/share/virtualenvs/sf_cs328-crowdsourced-QAuuIxFA/lib/python3.5/abc.py", line 225, in __subclasscheck__
    for scls in cls.__subclasses__():
TypeError: descriptor '__subclasses__' of 'type' object needs an argument

If you remove parameters until the exception disappears, we find that the problem type is the first: exc_type: Optional[Type[BaseException]].

Note: To get it to complain if the type signature does not match (when run with mypy), you need to change class Foo: to class Foo(ContextManager['Foo']). I didn't do this in the snippet, because typing in Python 3.5.2 is missing Coroutine, Awaitable, ContextManager, etc (this is the version in LTS releases of old-ish distros). I wrote about a workaround for this here: https://stackoverflow.com/a/49952293/568785. So I guess, the full reproducible example is:

# Workaround for ContextManager missing in 3.5.2 typing
from typing import Any, TypeVar, TYPE_CHECKING
try:
    from typing import ContextManager
except ImportError:
    class _ContextManager:
        def __getitem__(self, index: Any) -> None:
            return type(object())

    if not TYPE_CHECKING:
        ContextManager = _ContextManager()

# The actual issue:
from types import TracebackType
from typing import Optional, Type

class Foo:
    def __enter__(self) -> 'Foo':
        return self

    def __exit__(self, exc_type: Optional[Type[BaseException]],
                 exc_value: Optional[BaseException],
                 traceback: Optional[TracebackType]) -> bool:
        return False

I've verified that not inheriting ContextManager for running in Python 3.5.2 still produces the error (so this exception isn't a product of this hack, it's a product of the 3.5.2 runtime's typing library not liking the signature of __exit__).

Presumably, this is another bug in Python 3.5's typing library. Is there a sane way to work around this?

like image 332
Bailey Parker Avatar asked Apr 21 '18 19:04

Bailey Parker


People also ask

What is pep484?

PEP 484, which provides a specification about what a type system should look like in Python3, introduced the concept of type hints.

What is TypedDict?

A TypedDict is a new kind of type recognized by Python typecheckers such as mypy. It describes a structured dictionary/map with an expected set of named string keys mapped to values of particular expected types. Such structures are ubiquitous when exchanging JSON data, which is common for Python web applications to do.

Should I use type hinting in Python?

Type hints help you build and maintain a cleaner architecture. The act of writing type hints forces you to think about the types in your program. While the dynamic nature of Python is one of its great assets, being conscious about relying on duck typing, overloaded methods, or multiple return types is a good thing.

What is optional in Python typing?

Use Optional to indicate that an object is either one given type or None . For example: from typing import Dict, Optional, Union dict_of_users: Dict[int, Union[int,str]] = { 1: "Jerome", 2: "Lewis", 3: 32 } user_id: Optional[int] user_id = None # valid user_id = 3 # also vald user_id = "Hello" # not valid!


1 Answers

A ugly workaround that I'm using for now, is use TYPE_CHECKING to determine if I should just fake the type:

from typing import Type, TYPE_CHECKING

if TYPE_CHECKING:
    BaseExceptionType = Type[BaseException]
else:
    BaseExceptionType = bool # don't care, as long is it doesn't error

Then you can do:

def __exit__(self, exc_type: Optional[BaseExceptionType],
             exc_value: Optional[BaseException],
             traceback: Optional[TracebackType]) -> bool:
    return False

Verified against Python 3.5.2 and mypy 0.560.

This of course breaks any RTTI, but AFAIK RTTI is part of a PEP that didn't land (experimentally) until 3.6 or 3.7. This does break typing.get_type_hints() though, obviously.

like image 193
Bailey Parker Avatar answered Oct 12 '22 14:10

Bailey Parker