object.__hash__ object.__eq__ and Hashable are currently underspecified in the type system. object.__hash__ and object.__eq__ are currently assumed to exist on object, and the types they have do not match the intended use
This becomes a problem for LSP as the design of the language is that these are removable disableable qualities that happen to have a default behavior, not that the default behavior is intended for all types inheriting from object.
- The data model specifically allows for overriding
__eq__to return non-bool objects (see ORM use) - defining
__eq__without defining an updated__hash__removessets__hash__toNoneinstead - hash can be directly set to
Noneto remove hashability.
Proposed changes:
- The type of
__hash__considered on object isNone | ((self) -> int) - The type of
__eq__on object is(self, other: Any, /) -> Any - It is determined when declaring a type by the type system if a type would be Hashable by following the rules of the language. (the presence of a definition of
__eq__without a corresponding definition of__hash__, as well as explicitly setting__hash__ = Noneremove hashability) - It is an error to remove hashability when subclassing any type other than object which has hashability.
- It is an error to add hashability when subclassing a type that does not have it.
- type checkers should determine that instances of the exact type
objectare bothobjectandHashable[1] (ie. in the case ofx = object(), type checkers should preserve the knowledge that x is Hashable in that local scope.)
This results in a consistent interface, at the cost of the type system not accepting a few extremely suspect design choices that someone could make (such as emoving hashability in one type, then re-adding it in a further subtype, then removing it in yet a further subtype while then expecting all of those types to automatically work with subtyping based rules for Hashable), and at the cost of a limited amount of extra tracking using an existing protocol (Hashable) to handle cases where object() is used
The special casing on object is limited and defined in such a way that it limits the impact to cases that can already cause issues such as:
def problem(x: object, mapping: dict[Hashable, Any]):
mapping[x] = x
Which could be immediately fixable in those cases to:
def fixed(x: Hashable, mapping: dict[Hashable, Any]):
mapping[x] = x
Below are substantial additions taken from the discussion below for a one-place summary, these are intentionally tacked on at the bottom for convenience but not adding things which mailing list users would be unaware of here.
Why is adding hashability also an LSP violation?
It’s not possible to not have a definition of __hash__ on a type. The interpreter removal of it sets it to None, the supported user removal directly requires setting to None. It’s also not possible even for types defined in C-extensions, __hash__ is a required implementation detail.
Adding hashability to an unhashable type therefore looks like:
class Unhashable:
__hash__ = None
class Problem(Unhashable):
def __hash__(self) -> int: ...
Leaving the special casing to object simplifies the number of cases type checkers need to consider, and there are not practical reasons to allow this, subclassing should be cooperative in nature, building off of types that are appropriate to build onto for your use.
Handling of Any
(Thanks @Liz , see comment below)
In the case where subclassing has happened involving Any, if any non-object, non-Any type’s Hashability can be determined, including the definition of the type being defined, Any is assumed compatible and that definition is used.
In the indeterminable case, the assumption is that the type is hashable, preserving compatibility with Hashable. This is a type safety hole, but not a hole in consistent definitions of Any as a compatible type. It is trivially fixable by setting hash to None when subclassing untyped unhashable types.
while intersections are not denotable, the ability of type checkers to track such a type is required already by
typing.TypeIs, and would only need to be tracked in the local scope where this happened, as the non-denotability meansobject & Hashableis not a valid parameter or return type ↩︎