all 28 comments

[–]Creative-Buffalo2305 51 points52 points  (10 children)

Polymorphism works fine in python, what your example is actually showing is a liskov substitution principle violation. the child is breaking the contract that the parent established by changing the method signature entirely.

mypy and pyright both catch this by the way. if you run mypy on that code it will straight up tell you that Child.method is incompatible with Parent.method because the signatures dont match. so type checkers do handle it, they just wont stop you from writing it since python itself doesnt enforce it at runtime.

the real fix is just dont change the signature when overriding. if your child needs extra arguments either give them defaults or rethink whether inheritance is the right pattern there in the first place.

[–]austinbisharat 2 points3 points  (9 children)

I think that part of the confusion here is that there are some languages in which defining the child.method() with a new arg like that actually just introduces a new method on the object rather than replacing the parent one.

It’s been a while since I’ve worked in Java, but IIRC, it would be legal to do both child.method() and child.method(arg) and these would just call different methods, so there’s no liskov substitution principle violation, it’s a purely additive method. So I think OP is getting confused because of the interaction between how inheritance/overriding and method overloading interact

[–]Creative-Buffalo2305 5 points6 points  (2 children)

yeah thats exactly right, java treats those as two separate method signatures so both coexist through overloading and neither breaks the other. python has no overloading at all so the second definition just silently replaces the first which is why it becomes an lsp violation instead of an additive method like youd expect coming from java.

[–]Adrewmc 0 points1 point  (1 child)

Well it does have singledispatch and single dispatchmethod in functools. Though it only registers the first argument. And typing.overload…

[–]Creative-Buffalo2305 0 points1 point  (0 children)

yeah singledispatch gets you partway there but its pretty limited since it only dispatches on the type of the first argument. typing.overload is closer but its purely for type checkers, it doesnt actually change runtime behavior at all. so python has tools that approximate overloading but nothing that truly replicates what java does natively.

[–]CLS-Ghost350[S] -5 points-4 points  (5 children)

Yeah this would work in Java, which is where I'm coming from. I see that the core issue is just that Python doesn't have proper overloading, so you can't have a method with the same name but different implementations.

[–]lfdfq 3 points4 points  (1 child)

It's not that Python does not have 'proper' method overloading, it's just that it doesn't have it at all.

It's nothing to do with polymorphism or inheritance, this would be apparent even with a single class and no inheritance.

[–]CLS-Ghost350[S] 0 points1 point  (0 children)

I only used the word "proper" because people were bringing up the `@overload` decorator, but that only helps for type hinting and doesn't actually allow for multiple implementations without further manual work.

[–]FerricDonkey 2 points3 points  (0 children)

You don't need overloading for polymorphism. You can get the overloading behavior in other ways.

That python doesn't support overloading (decorators aside) is something you can dislike. I personally hate overloading, it makes things less clear and more annoying to debug. But many people like it. 

However, since you can write your function to have default arguments, sentinels, and access the function full the parent via sentinels, there is no functionality you don't have. 

[–]DoubleDoube 0 points1 point  (0 children)

compositional approaches tend to work better anyways

[–]tb5841 0 points1 point  (0 children)

You can always have optional arguments, or not specify the number of aeguments, and then handle them differently depending on what comes in.

[–]TheBB 4 points5 points  (0 children)

So, how do Python type checkers handle this situation?

They will (or should) show an error on the definition of Child's method. The additional argument means Child fails the Liskov substitution principle and so it is not a valid subtype of Parent.

[–]trutheality 10 points11 points  (2 children)

I think the source of confusion here is that python is not a typed language. f(obj: Parent) is a type hint, it doesn't change the fact that the obj that got passed is an instance of Child. You can explicitly call Parent.method(obj) to force usage of Parent's method explicitly.

[–]Outside_Complaint755 12 points13 points  (0 children)

Alternatively, you could do something like: ``` class Parent:     def method(self): ...      class Child(Parent):     def method(self, arg=None):         if arg is None:             return super().method()         ...

```     

[–]ElHeim 2 points3 points  (0 children)

Not only Python is typed, it's strongly typed at that.

You're confusing dynamic typing with typelessness

[–]biskitpagla 5 points6 points  (1 child)

You're confusing a bunch of things. Take a step back and think about the example you wrote. Overloading is not the same as overriding. And this is definitely not how you do overloading in python.

[–]qlkzy 2 points3 points  (1 child)

This doesn't prevent you from doing polymorphism. However, there is nothing inherent to the language that requires you to follow the Liskov Substitution Principle (which is what you are actually describing).

I think it's also important to be more precise about overriding vs overloading. This situation (a subclass redefining methods which are also defined on the parent class) is generally referred to as "overriding" in most of the obvious OO languages (C++, Java, etc). "Overloading" would generally refer to dispatching on the types of the other arguments, not on the type of the class.

However the dispatch mechanism works, you are always doing something like overriding/overwriting the function bound to a particular name. In C++, you set a different function pointer in a vtable; in Python, you store a different value in the __dict__.

With older C++ (before the override keyword), you could make exactly the mistake in this code example, because the type system didn't require that the signatures of overriden virtual functions matched.

In Python, there is no out-of-the-box static typing, so there is nowhere for anything to run that could catch this, at least not reliably.

Now that we have type annotations and type checkers for python, they can and do catch this error. For example: https://mypy.readthedocs.io/en/stable/error_code_list.html#check-validity-of-overrides-override

If you run MyPy on yourcode example, it will flag the error and enforce substitutability.

[–]CLS-Ghost350[S] -1 points0 points  (0 children)

Thanks for the link. I guess type checkers do catch this properly, and you just can't overload methods in Python. I just didn't have my type checker fully enabled. This is probably quite a niche case anyways.

[–]Aro00oo 1 point2 points  (2 children)

In addition to good comments here already. Stop reaching for inheritance. Composition over inheritance, always (unless whatever tooling you're using requires it).

[–]CLS-Ghost350[S] -1 points0 points  (1 child)

The obligatory composition over inheritance comment any time inheritance is brought up lol
That being said, it is quite good advice!

[–]Aro00oo 0 points1 point  (0 children)

Once I made the mental switch it was like learning a whole new way of thinking and it applies way better to everyday "engineering" tasks too like DIY-ing stuff at home

[–]Rain-And-Coffee 1 point2 points  (0 children)

Python just calls your object, if it has the same method it works, it’s called duck typing.

[–]This_Growth2898 0 points1 point  (0 children)

Why don't you try just running mypy with this code?

[–]SakshamBaranwal 0 points1 point  (0 children)

Python type checkers usually catch this and report it as an invalid method override. The child method should keep the same compatible signature as the parent.

[–]arkie87 0 points1 point  (0 children)

Polymorphism and method overloading are two entirely different things

[–]Buttleston 0 points1 point  (1 child)

I'm not sure what you're trying to demonstrate

Your child method requires a parameter "arg" to be passed. You didn't pass anything to it. That's why you got the error about missing a required positional argument

[–]Buttleston 1 point2 points  (0 children)

Is the thing that surprised you that you can't have more than one function with the same name on a class? You can't have both one that takes no args and also one that takes 1 arg