I'm afraid, Oscar, that you seem to have painted yourself into a reductio ad absurdum.  We need a healthy dose of "practicality beats purity" thrown in here.

What the substitution principle essentially says is
   if x == y then f(x) == f(y)
for any function f such that f(x) is well defined.
I've come to the seemingly obvious conclusion that if there is *any*
difference between x and y then it's always better to say that x != y.
 
>>> x = 3.1415
>>> y = 3.1415
>>> x == y
True
>>> f = id
>>> f(x) == f(y)
False

I'm very happy to agree that "but id() isn't the kind of function I meant!"

That's the point though.  For *most* functions, the substitution principle is fine in Python.  A whole lot of the time, numeric functions can take either an int or a float that are equal to each other and produce results that are equal to each other.  Yes, I can write something that will sometimes overflow for floats but not ints.  Yes, I can write something where a rounding error will pop up differently between the types.  But generally, numeric functions are "mostly the same most of the time" with float vs. int arguments.

This doesn't say whether tuple is as similar to list as frozenset is to set.  But the answer to that isn't going to be answered by examples constructed to deliberately obtain (non-)substitutability for the sake of argument.


--
The dead increasingly dominate and strangle both the living and the
not-yet born.  Vampiric capital and undead corporate persons abuse
the lives and control the thoughts of homo faber. Ideas, once born,
become abortifacients against new conceptions.