◐ Shell
clean mode source ↗

Fix generics inheritance and drop __subclasscheck__ by bintoro · Pull Request #207 · python/typing

I don't expect this to be reviewed anytime soon, but the effort is now fairly complete, so might as well put it out there for comments.

Resolves #136
Resolves #202
Resolves #203

At the heart of #136 is a conflation between type compatibility and subtype relationships. Even though Sequence[Manager] is compatible with Sequence[Employee], it's not a subclass of it. Rather, both derive from Sequence[+T_co].

To address this, the PR introduces a new helper is_compatible(), which implements the "is consistent with or a subtype of" relation. In short, it does what issubclass() has been doing until now.

The practical end result is that

  • issubclass() is no longer applicable to the special types Any, Union, etc.

  • is_compatible() always works — if neither argument is a special type, is_compatible(T1, T2) reduces to issubclass(T1, T2)

  • issubclass() acquires its normal semantics w.r.t. generics:

    >>> issubclass(Sequence[Manager], Sequence[Employee])
    False
    >>> issubclass(Sequence[Manager], Sequence)
    True

    whereas:

    >>> is_compatible(Sequence[Manager], Sequence[Employee])
    True

Commit summary

There are three commits:

  • The first simply stops the __extra__ linkage from leaking down to generics' subclasses. This fixes the bug where issubclass(dict, CustomMapping) == True. [Diff]
  • The second adds the __extra__ ABCs as actual base classes of their typing twins. It also refactors GenericMeta.__subclasscheck__ into a compatibility check and a __subclasshook__ dedicated to handling the __extra__ connection. [Diff] This achieves a few things:
    • User-defined generics become recognized as subclasses of the associated collections ABCs.
    • Abstract and mixin methods from collections.abc become available.
    • issubclass(list, Iterable[X]) no longer returns true for arbitrary X (Kill __subclasscheck__ #136).
  • The final commit is largely a mechanical change: the special types are made to raise a TypeError in response to issubclass(). Every type metaclass gets a method named __is_compatible__ to host the prior __subclasscheck__ logic. [Diff]

Notes

  • Full parity is preserved in using unparameterized types in place of the corresponding collections.abc classes in isinstance() and issubclass() calls:

    issubclass(type, typing.Callable) == issubclass(type, collections.Callable) == True
    issubclass(list, typing.Iterable) == issubclass(list, collections.Iterable) == True
    ...
  • The intent has been not to mess with the types' behavior. That said, the introduction of is_compatible() yields a couple of automatic improvements regarding Any:

    • The relation evaluates to true when either argument is Any; previously issubclass(Any, C) would return false for some arbitrary class C but true for a typing type.
    • is_compatible(Iterable[Any], Iterable[int]) correctly returns true.
  • The merging of Specific asserts #205 caused a huge conflict in the tests. I will look into it eventually if there's interest in merging this.

  • The proposal is compatible with the plan to convert some of the collection types to protocols, if that's still on the table.