Skip to content

Odd types.get_original_bases() behavior for classes with generic bases but no type arguments #107576

Closed
@chrisbouchard

Description

@chrisbouchard

Bug report

Checklist

  • I am confident this is a bug in CPython, not a bug in a third-party project
  • I have searched the CPython issue tracker, and am confident this bug has not been reported before

A clear and concise description of the bug

(I originally posted about this on python-list a little over a week ago, but didn't get any replies.) I was playing around with 3.12.0b4 recently and noticed an odd (to me, at least) behavior with types.get_original_bases().

>>> T = typing.TypeVar("T")
>>> class FirstBase(typing.Generic[T]):
...   pass
... 
>>> class SecondBase(typing.Generic[T]):
...   pass
... 
>>> class First(FirstBase[int]):
...   pass
... 
>>> class Second(SecondBase[int]):
...   pass
... 
>>> class Example(First, Second):
...   pass
... 
>>> types.get_original_bases(Example)
(__main__.FirstBase[int],)
>>> Example.__bases__
(<class '__main__.First'>, <class '__main__.Second'>)
>>> types.get_original_bases(First)
(__main__.FirstBase[int],)

In summary, types.get_original_bases(Example) is returning the original base types for First, rather than its own.

I believe this happens because __orig_bases__ is only set when one or more of a generic type's bases are not types. In this case both bases are types, so Example doesn't get its own __orig_bases__. Then when types.get_original_bases() tries to get __orig_bases__ on Example, it searches the MRO and finds __orig_bases__ on First.

The same thing also happens if all the bases are “bare” generic types.

>>> class First(typing.Generic[T]):
...   pass
... 
>>> class Second(typing.Generic[T]):
...   pass
... 
>>> class Example(First, Second):
...   pass
... 
>>> types.get_original_bases(Example)
(typing.Generic[~T],)

I'm not entirely clear if this is a bug, or an intended (but unfortunate) behavior. I would personally expect types.get_original_bases() to check if the type has its own __orig_bases__ attribute, and to fall back to __bases__ otherwise.

For example, the way it works currently makes it unnecessarily difficult to write a function that recurses down a type's inheritance tree inspecting the original bases—I currently have to work around this behavior via hacks like checking "__orig_bases__" in cls.__dict__ or any(types.get_original_bases(cls) == types.get_original_bases(base) for base in cls.__bases__). (Unless I'm missing some simpler solution.)

Is this something that could (should?) be addressed before 3.12 lands?

Your environment

% docker run -it --rm python:3.12-rc
Python 3.12.0b4 (main, Jul 28 2023, 03:58:56) [GCC 12.2.0] on linux

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions