Too many examples that you find are (IMO) fatally flawed. The Animal->Dog is especially flawed, though widely used. The problem is that these sorts of examples almost require that the superclass has a certain set of public methods that isn't the same as that of the subclass, requiring you to add additional public methods to the subclass. This is because an object of the subclass has different behavior than that of the superclass. This leads to poor design. If, when you create a variable you think of it as an Animal (or in a statically typed language declare it so) then later, when you assign a Dog object to the same variable you, the programmer have to remember that it is now a dog so that it can accept the sit() method that wasn't implemented in Animal.
In a dynamically typed language like Python, the programmer has to remember types since the compiler doesn't, but if you also have to remember where in an inheritance hierarchy an object is at runtime then you have a more difficult than necessary task. You can ask an object for its type of course, but that leads to messy code.
A better solution is to maintain the set of methods declared in the topmost class as you descend and not extend them. What you change in the subclass is only the implementations of methods defined in the superclass. Then your original intention (it is an Animal) doesn't need to be modified (and mentally maintained) as you write the program. The variable responds only to Animal methods, clearly impossible in this example.
One way to help solve your problem is to assure that your inheritance hierarchies are very (very) shallow. Don't try to define things three or four deep. This helps even if you do add additional public methods in a subclass.
However, there is a better way.
One reason for building hierarchies is to get different behavior in the subclass. But there is a better way to do that. And it doesn't require adding additional behaviors. The flaw in thinking is that many people think of using inheritance hierarchies for BIG things. Like Dogs and Giraffes. If instead you don't use inheritance for the big things but only for small things, life gets easier.
The alternative to inheritance I suggest is to build objects by composition. Complex (i.e. big) things are made up of smaller parts that are themselves complex (objects, not integers and strings). This leads to programming by delegation. An object can delegate some of its behavior to another object, possibly held in a field and invisible to the public class's clients. When a public method of the class is invoked, it simply sends a message to the delegate object and can get a return value that it can return or modify.
So the makeNoise() method of Animal looks like this.
def makeNoise():
myNoiseMaker.shout()
return
The object in myNoiseMaker makes the actual sound. But by changing that object the Animal can bark or meow or roar or what ever is needed for that animal. The value can be set in a constructor to produce a Dog or a Lion, of course. All animals behave the same (same set of public methods) but each in its own way. Don't start thinking that we need switch statements to achieve this. There is no test for the kind of thing we are. One possible value of myNoiseMaker only knows how to bark. A different object knows how to roar. We just create (or replace) the object as needed.
This of course is well known as the Strategy Design Pattern. The strategy here is an object with one method: shout()
.
It is also possible to use inheritance at this level and one Strategy can inherit from another. Usually you want a Strategy that implements shout as a no-op. You can inherit from this to create a Dog and from that to create a LoudDog. So an object built by composition (lots of Strategies) and using Delegation is pretty rich and very flexible. Each instantiation of an Animal object only knows how to do one thing as appropriate. This is polymorphism, actually.
But, once you grasp the idea of a Strategy you can do much more. As a program runs, its state changes. One possible way to develop programs is with State Change Diagrams. At certain state changes it is possible to replace one delegate with another, changing the behavior of the containing object that uses delegation. For example, the first time you press a number key on a calculator it shows that value in the display. But the next time you press it something different happens and the new value is accumulated into the old. So pressing a number key after pressing the equals key has different behavior than after pressing a number key. This can be handled by having the calculator delegate the key presses to (various) strategy objects as the state changes. Again, you don't need flags and switch statements to remember what to do next. The Strategy object knows what to do since it was designed to do only that one thing.
To go even farther, another design pattern is Decorator. There are various kinds of decorator but the essence is that it is a certain kind of thing and it also has something of the same kind. A Strategy Decorator is a strategy (it has the shout method) and it also has a strategy as a field. When the containing object delegates to the Decorator it can first fire the shout method of the held object and and also add a sound of its own. Since a Decorator is a Strategy it can also decorate another decorator as the held object. This is essentially a linked list of Strategies with all but the last being a Strategy Decorator.
So, you can illustrate inheritance and dynamic polymorphism very nicely with small classes such as strategies and decorators. But it requires a mental model about how to build objects - use composition primarily (for the big things) and inheritance for the small things.
And notice that this mechanism (composition + delegation) moves the focus of change-of-behavior from the class to the individual object.
To more explicitly answer your question, you could provide a basic class in which a few methods delegate to other objects and then have them make the behavior of objects more interesting by defining other delegates that inherit from the ones you give. The first cut could have all of the delegates provide empty behavior. Rather than modifying the classes of those objects, have them write (simple) subclasses.
I'll note in also that a statically typed language such as Java can do even better here, since the explicit declarations of the variable types, unavailable in Python and Ruby, makes it clear about what is a Strategy and what is not. In general, though a Strategy is very simple, a method or two, named for the task at hand.
Let me try to explain why it is a bad idea, especially in statically typed languages like Java but also in Python, to define new public methods in a subclass or in classes that implement an interface.
Suppose you have the following: Animal (either a base class or an interface, and you have Mammal and Invertebrate as subclasses of Animal. You also have Dog and Kangaroo as subclasses of Mammal and Mosquito as a subclass of Invertebrate. Suppose also that each class introduces new public methods not defined in its superclass. The sensible programmer will of course create a Dog object with
Dog myPet = new Dog(Fido);
and other objects similarly. Now myPet.sit(); makes perfect sense whereas had myPet been given the static type Animal it would not.
However, the problem arises when you create collections, say List, or pass myPet as an argument to a method that expects an Animal. What can you do in that method? What can you do with objects extracted from the List? The compiler only knows the object as Animal, of course, so you can invoke those methods, but what if you want to do more with the object. Runtime type checking (instanceof) and Casting can recover the specific type but at the cost of potential runtime errors if you make inappropriate assumptions. The original programmer may not make such errors in a small program, but the problem becomes very difficult in a large, important, and maintained-by-others program. It isn't the original declaration that is the issue here. Of course you say Dog. Or do you?
A very common idiom in Java is to declare an ArrayList as follows
List<Animal> animals = new ArrayList<Animal>();
Note the interface type on the LHS and a specific class type on the RHS. This makes it easy to replace the concrete type with a different one without changing the LHS or the code that follows. This is actually the preferred method of doing such declarations.
Why does it work? It works because all of the provided implementers of List have, as their only public methods, the methods defined in List itself. It wouldn't work if that were not the case. Then, if additional methods were defined (and used), changing the concrete type would become much more difficult.
The good practice in the libraries (not adding public methods) is good for a reason. I'm simply suggesting it as a general practice that makes for better programs. It is also a simple and safe rule for beginners to learn. The rule is frequently broken, I realize. But poorly designed software is frequently written, also.
And note that this problem is more severe in Python, which is dynamically typed and variables (identifiers) have no type at all. Java declarations at least give you some sort of managed knowledge of what sort of thing is referenced. In Python (Ruby) you need to do that yourself or include frequent type checks or suffer frequent errors.