`__hash__`, `__eq__`, and LSP

I disagree on both counts: It is possible to have static type checking without LSP, and LSP is not the best system to achieve static type checking.

The best system to achieve static type checking is what we call Protocol in Python, and what in Rust are called traits.

In practice I see that system works:
Rust does not have LSP. (Rust doesn’t even have inheritance.) Yet Rust does have incredibly good static type checking.
Rust has traits. ( impl - Rust ) .
In Rust you can declare which traits a type must have to be accepted by a function ( Traits: Defining Shared Behavior - The Rust Programming Language ) .
And famously Rust is incredibly type safe, more so than Java and C++/#.

I haven’t worked as a professional Python developer long yet. In the situations where type-hinting a class or a union of primitive python types was insufficient, I was able to type hint a Protocol, and it worked well. Pylance was able to pick up the type guarantees, etc. I had all the benefits of static type checking.

I don’t see how information from a Protocol can be incorrect.
LSP in contrast sometimes just doesn’t deliver what it promises.

LSP also places hard-to-satisfy demands on your code, such as the suggestion made in the OP of this thread. And relying on LSP encourages you to create deep inheritance trees, which I personally find very hard to work with. (And I feel they make programming error-prone.)
Protocols in contrast only require you to define your protocols.

Yeah, so that’s the consequence of attempting (to rely on) LSP.
If these functions were systematically type-hinted with Mapping & Hashable or Mapping & Updatable, or just plain Mapping, the static type checker would know immediately which functions worked with frozen/dict.
Instead, we have to try and see whether the function throws an error.
Which, fortunately, isn’t a lot of work in Python.
(And there’s conventions that aren’t quite formalized, that dict usually means the same as plain Mapping, whilst frozendict means Mapping & Hashable. Which helps further reduce the workload. I just mean to stress the current situation is workable.)
(edit: functions that I worked with that are type-hinted with dict often also rely the | operator, which I don’t think is included in the Mapping protocol. Which makes ‘correct’ type hinting more complicated.)

(I don’t know whether it is possible to specify a function input needs to satisfy 2 Protocols because I never needed it. But if it were possible I expect it would look like Mapping & Hashable.)

I don’t disagree with you that nominal typing with subclassing has drawbacks, and structural typing has advantages.

But, unlike Rust, Python does have subclassing, and the Python static type system does have nominal typing with subclassing, and neither of those facts will change.

This means that even if you prefer to use structural typing, the structural types have to coexist with nominal types, and we have to be able to decide which nominal types are assignable to which structural types. Doing this soundly requires LSP.

As @mikeshardmind pointed out already, this entire proposal is actually an effort to make structural typing with a Hashable protocol – exactly the sort of typing you say you want! – possible!

It doesn’t do much good to annotate a parameter as Mapping & Hashable if the type checker has no way to decide whether a given nominal type C does or does not satisfy the Hashable protocol. Since the nominal type C includes subclasses of C, enforcing LSP for hashability of nominal types is the only way a type checker can possibly know that.

(Python does not currently have intersection types, so it isn’t currently possible to annotate Mapping & Hashable, but I hope that it will become possible in the future. I agree that this makes structural types more useful.)

3 Likes

LSP isn’t going anywhere. I agree with you, people shouldn’t use inheritance anywhere near as much as they do. Most things people toss in with a subclass should be functions accepting the original class, and not methods of a subclass. But python has subclassing, so the type system has to understand the impacts of when people do that.

You might want to write Liskov Substitution Principle (LSP) for this initial instance. It took me far too long to remember that the Liskov substitution principle has those initials: before that, my hypothesis involved a conflation of Language Service Protocol with type checking …

(I was going to be all fancy and use <abbr>, but then I realized I couldn’t get the title value to show up on my phone.)

2 Likes

Are you sure this was not in reference to Lumpy Space Princess?

2 Likes

Thanks, I’ll go ahead and edit the above for clarity (ed. post is too old) try to remember to expand on abbreviations at first use going forward . It’s easy to forget when you get into the weeds of a topic that some abbreviations and jargon aren’t always what people will think of when presented with it

1 Like