[Libre-soc-dev] liskov substitution principle and type assertions/annotations

Jacob Lifshay programmerjake at gmail.com
Wed Aug 10 05:20:19 BST 2022


On Fri, Aug 5, 2022, 19:59 lkcl <luke.leighton at gmail.com> wrote:

> i'm going to be blunt: tough.   python != "most other programming
> languages".
>
> please get it into your head that python's strength is its use of
> the liskov substitution principle.
>
> types destroy that <snip>
>

Since the LSP isn't mentioned in Python's docs, even once, I will use the
definition from:
https://www.pythontutorial.net/python-oop/python-liskov-substitution-principle/

> The Liskov substitution principle states that a child class must be
substitutable for its parent class. Liskov substitution principle aims to
ensure that the child class can assume the place of its parent class
without causing any errors.

Therefore code like the following is perfectly acceptable according to the
LSP:

class C:
    field1: float
    field2: int
    def f(self, arg: int) -> str:
        assert isinstance(arg, int)
        return str(arg)

because any subclass of int can be passed as arg, and any subclass of str
returned from f and any subclass of float assigned to field1. restricting
the allowed types is perfectly fine because there isn't a superclass of C
that those field/argument/return types have to be compatible with, so there
is no issue.

The part where LSP is violated is where a subclass changes the superclass's
existing API to do something the superclass disallows:
class D(C):
    field1: int # not valid because C allows assigning c.field1 = 3.5 and D
doesn't
    field2: "int | str" # not valid because C guarantees c.field2 returns
an int and D doesn't
    def f(self, arg: bool) -> "str | bytes":
        assert isinstance(arg, bool) # not valid because C.f allows any int
and D.f doesn't
        return bytes(arg) # not valid because C.f guarantees returning str
and D.f doesn't

using assert type(arg) == int is generally not allowed because it doesn't
allow any int subclasses, whereas using assert isinstance(arg, int) *is
allowed* because it still works with all int subclasses.

Note that adding new API is fine, just breaking existing API is a problem:
class F(C):
    field3: "list[int]" # fine because it didn't exist in C
    def f(self, arg: float) -> str:
        # fine because everything C.f can be called with (ints),
        # F.f can too because all ints are floats
        assert isinstance(arg, float)

        if isinstance(arg, int):
            return str(arg) # needed to preserve behavior for int arguments

        # fine because we're returning strings like we said and
        # C.f's documented behavior is preserved
        return "blah"

In summary, type assertions and annotations do *not* violate the LSP in and
of themselves, they only violate the LSP when they do something the
superclass is not allowed to or don't allow something guaranteed allowed by
the superclass.

Therefore, using the LSP as the reason to not allow any type annotations or
instanceof checks whatsoever is not a logical justification for why we
shouldn't allow them.

Other reasons to not use type annotations may still be valid, such as the
reasons lkcl mentioned that he thinks they're illegible or confusing to
python beginners.

I think that type instanceof assertions (and assertions in general) are
beneficial because they simultaneously serve several purposes:
1. to prevent bugs where the caller passes in things where the function is
not designed to handle them, such as a function for sign extending an
integer that expects a positive integer bitwidth of the result, and you
passed in a float instead, it should assert and fail immediately, rather
than returning a bad result that will be propagated through lots of code
and be very hard to spot (this happened with migen a while ago with a float
signal width https://github.com/m-labs/migen/pull/262 ).

2. as documentation for what types the code is designed to operate on --
code is often undocumented or has documentation missing important details,
having type assertions helps the user figure out how to call that function
without having to look through heaps of code and guess and sometimes guess
wrong. e.g. code that assumes some list has no duplicates can assert there
are no duplicates rather than silently returning the wrong result. that
assertion also serves as documentation that it expects no duplicates at
that point in the code.

3. type assertions also help IDEs give you the correct information, e.g. if
it knows some variable v is a str then it can autocomplete str's methods --
when you type v.isd it can give you v.isdigit. when you hover over a
variable, it can tell you it's a str. when you do something like try to
append it to a list[int] variable, it can tell you that you messed up there
because str isn't int. This is very beneficial as, in my experience, it can
speed up writing code by a factor of 2x or more.

Jacob

>


More information about the Libre-soc-dev mailing list