microsoft / pyright

Static Type Checker for Python
Other
13.1k stars 1.4k forks source link

no error when using generic in `except` block #7921

Closed DetachHead closed 3 months ago

DetachHead commented 3 months ago
from typing import Generic, TypeVar

T = TypeVar("T")

class Foo(Exception, Generic[T]): ...

try:
    raise Foo
except Foo[int]: # no error, crashes at runtime
    ...

playground

this should be a type error because it fails at runtime with the following exception:

TypeError: catching classes that do not inherit from BaseException is not allowed
erictraut commented 3 months ago

Pyright doesn't currently implement this check. It's not really a type checking issue, so a type checker isn't under obligation to catch errors like this one. I'll switch it to an enhancement request and consider it in that light.

It's not clear to me whether this is a bug in the runtime. I would have assumed that this was legal, so it's a surprise to me that it's not. Perhaps it was just an oversight when generic support was added to the language. Do you have any information that confirms this isn't simply a Python bug? Have you tried reporting it?

I suspect that no one has ever tried to do what you're doing here, so maybe it has never come up before. I don't think this is a common source of errors.

I'm going to close the enhancement request for now, but I'm willing to reconsider in the future if there's signal from other pyright users (in the form of additional upvotes on this issue) that this is important to them.

One-Nose commented 3 months ago

This isn't a Python bug, it's how generics work. Foo[int] isn't a class, it's a generic alias object.

>>> from typing import *
>>> T = TypeVar('T')
>>> class Foo(Exception, Generic[T]): ...
...
>>> type(Foo)
<class 'type'>
>>> type(Foo[int])
<class 'typing._GenericAlias'>

Foo[int] only functions as a subclass of Foo when treated as a type annotation, but it's an unrelated object during runtime. Pyright in this code segment accidentally reads it as if it was an actual class.

erictraut commented 3 months ago

it's a generic alias object

That's an implementation detail, not part of the mental model for most developers. In most cases, Foo[int] acts like Foo at runtime. For example, you can call Foo[int] to invoke Foo's constructor, and Foo[int].x accesses class variable x on Foo. Clearly, the implementers of generic types went through some trouble to make it appear from a developer's perspective that Foo[int] is a class. It's not clear to me whether it was an oversight that Foo[int] cannot be used in an except statement.

DetachHead commented 3 months ago

i agree with @erictraut here. the internal implementation details of python's type system seem to often come up when type annotations get evaluated at runtime and are a massive source of confusion for developers, so much so that pyright warns against some of them. though i really think it's python's respnsibility to hide these internals from the user as much as possible. that seems to be the direction theyr'e taking since most of the runtime errors pyright warns you about were fixed several years ago.

i've raised it here: https://github.com/python/cpython/issues/119094

DetachHead commented 3 months ago

@erictraut it was closed with the reasoning that it also doesn't work in isinstancec checks. since pyright warns when you use use generic aliases in isinstance checks, could this issue be re-opened?

JelleZijlstra commented 3 months ago

Yes, I closed the CPython issue. Catching a generic alias doesn't make sense since the runtime can't know whether it's handling a Foo[str] or a Foo[int], especially in the exception catching code (which doesn't look at __instancecheck__ or other dynamic behavior).

Technically type checkers should probably catch this but it feels low priority, since it's unlikely to come up in practice.