There is currently no way in the type system to write an annotation that means “the set of all values that are instances of X
but not instances of subclasses of X
”. You can effectively do this if X
is marked @final
, which precludes the possibility of subclassing, but this isn’t possible for non-final
classes like int
.
Your proposed Just
protocol is clever, but it makes assumptions that are tenuous because it relies on an unsound type definition (an LSP violation), which leads to type checker behaviors that are undefined. Use it at your own risk.
As @mikeshardmind said, expressing the concept of “an instance of X
but not any of its subclasses” would require a new mechanism in the type system that doesn’t exist today.
This is something we could add if there’s sufficient need. I’ve heard this need expressed on a few occasions, but it’s relatively rare. Code that honors OOP principles shouldn’t check for a specific class X
but should rather use isinstance
and issubclass
to determine the compatibility of an object or class. I understand that some existing APIs break this rule and use is
rather than isinstance
or issubclass
, but I think this should be discouraged.
The annotation int | bool
describes exactly the same type as the annotation int
. The two types are equivalent. If a type checker treats them differently, that’s a clear indication of a bug in the type checker. In this case, pyright treats the two types as equivalent and mypy incorrectly treats them differently, so this is a bug in mypy. That said, I appreciate that detecting equivalency between types is difficult and costly (prohibitively so in some cases). If you search hard enough, I’m sure you would find cases where pyright’s behavior differs between two equivalent types.
You’ve pointed out a case where the overload resolution algorithm appears to treat arguments of type int | bool
and int
differently. In that example, however, the overloads are overlapping in an unsound way. If you decide to ignore this unsoundness, then uses of that overload may be unsound and produce inconsistent results. I don’t consider that a type checker bug.
The typing spec intentionally doesn’t mandate type checker behaviors based on unsound type definitions. Instead, it mandates that type checkers flag unsound type definitions and relies on users to fix those issues. If you create unsound type definitions (e.g. overrides that violate the LSP or overlapping overloads that have different return types) and ignore the unsoundness reported by the type checker, you are on shaky ground. Any type checking behaviors that you rely on after that point are type-checker-dependent and could change over time.
With your Just
class, you’ve created an unsound override of the __class__
symbol — one that violates the LSP, but you’ve chosen to ignore the unsoundness that pyright has reported.
You mentioned that overloads are complex and currently underspecified, which is true. In case you weren’t aware, there’s an effort underway to improve the spec in this area. This draft addition to the typing spec is nearing completion, and it defines behaviors for overloaded call evaluation and overlapping overload detection, among other things.