Skip to content

BaseExceptionGroup should return ExceptionGroup if initialized with non-base exceptions #12972

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Open
jakkdl opened this issue Nov 7, 2024 · 3 comments · May be fixed by #13554
Open

BaseExceptionGroup should return ExceptionGroup if initialized with non-base exceptions #12972

jakkdl opened this issue Nov 7, 2024 · 3 comments · May be fixed by #13554
Labels
stubs: false positive Type checkers report false errors

Comments

@jakkdl
Copy link

jakkdl commented Nov 7, 2024

When initializing a BaseExceptionGroup with non-base exceptions the stdlib (and the backport) will in fact return an ExceptionGroup. The typing in neither of typeshed nor the backport currently supports this.

from typing_extensions import reveal_type

x = BaseExceptionGroup('', [ValueError()])
reveal_type(x)
$ python foo.py
Runtime type is 'ExceptionGroup'
$ mypy foo.py 
foo.py:30: note: Revealed type is "builtins.BaseExceptionGroup[builtins.ValueError]"
$ pyright foo.py 
foo.py
  foo.py:30:13 - information: Type of "x" is "BaseExceptionGroup[ValueError]"

I have vague recollections that trying to do this was hard-to-impossible, but I currently cannot find any related issues.

@brianschubert
Copy link
Contributor

Somewhat related: #9922

At a glance it seems like an overloaded __new__ might work, similar to what's done with some of the other BaseExceptionGroup methods. Though given how special __new__ is, I suspect there may be some unique complications

(cc @sobolevn as the author of the current BaseExceptionGroup overloads)

@jakkdl
Copy link
Author

jakkdl commented Jan 28, 2025

Oh, found a comment in the test file noticing it as a limitation

# FIXME: this is not right, runtime returns `ExceptionGroup` instance instead,
# but I am unable to represent this with types right now.
assert_type(beg2, BaseExceptionGroup[ValueError])

and found the accompanying discussion #9230 (comment) where @sobolevn lays out the tradeoffs

I tried it out myself, and managed to repro the limitations*. Though I personally feel like getting BaseExceptionGroup -> ExceptionGroup is perhaps more important than custom BaseExceptionGroup subclasses; especially as end users can work around typing issues from custom BaseExceptionGroup with explicit type hints and/or adding a __new__ to their subclass.

Though I also hit python/mypy#17251 so we'd need to remove the __init__, and I'm not sure what the downsides of that would be.

* It's only a limitation for mypy, pyright handles this (somewhat minified) repro perfectly. Mypy falls back to defaults and raises some errors on the stub itself.

from __future__ import annotations
from typing import Generic, TypeVar, overload, Self
from typing_extensions import reveal_type

_BaseExceptionT_co = TypeVar("_BaseExceptionT_co", bound=BaseException, covariant=True, default=BaseException)
_ExceptionT_co = TypeVar("_ExceptionT_co", bound=Exception, covariant=True, default=Exception)

class BaseExceptionGroup(Generic[_BaseExceptionT_co]):
    @overload
    # mypy: Self argument missing for a non-static method (or an invalid type for self)
    def __new__(  # type: ignore[misc]
        cls: ExceptionGroup[_ExceptionT_co], _exception: _ExceptionT_co, /
    ) -> ExceptionGroup[_ExceptionT_co]: ...
    @overload
    def __new__(cls, _exception: _BaseExceptionT_co, /) -> Self: ...

    # mypy: "__new__" must return a class instance
    def __new__(  # type: ignore[misc]
        cls, _exception: _ExceptionT_co | _BaseExceptionT_co
    ) -> Self | ExceptionGroup[_ExceptionT_co]:
        return object.__new__(cls)

class ExceptionGroup(BaseExceptionGroup[_ExceptionT_co]):
    def __new__(cls, _exception: _ExceptionT_co, /) -> Self:
        return object.__new__(cls)

class MyBaseExcGroup(BaseExceptionGroup[_BaseExceptionT_co]): ...

class MyExcGroup(ExceptionGroup[_ExceptionT_co]): ...

reveal_type(BaseExceptionGroup(ValueError()))
reveal_type(BaseExceptionGroup(SystemExit()))
reveal_type(MyBaseExcGroup(ValueError()))  # mypy reverts to BaseException default
reveal_type(MyBaseExcGroup(SystemExit()))
reveal_type(ExceptionGroup(ValueError()))
reveal_type(MyExcGroup(ValueError()))

# expected errors
ExceptionGroup(SystemExit())  # type: ignore[type-var]  # pyright: ignore[reportArgumentType]
MyExcGroup(SystemExit())  # type: ignore[type-var]  # pyright: ignore[reportArgumentType]

@srittau
Copy link
Collaborator

srittau commented Feb 26, 2025

If someone could provide an exploratory PR, we could see the impact of such a change.

@srittau srittau added the stubs: false positive Type checkers report false errors label Feb 26, 2025
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
stubs: false positive Type checkers report false errors
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants