Programming Fundamentals & OOP
Inheritance and Polymorphism
Inheritance lets one class, a subclass, reuse and extend the attributes and methods of another, its base or parent class, instead of redefining them from scratch. Polymorphism lets code call the same method name on objects of different types and get behavior appropriate to each type, without checking which type it is first. The two ideas are usually taught together because inheritance is the most common way polymorphism gets set up, even though polymorphism itself does not strictly require inheritance.
Why it matters
- Avoids duplicating behavior shared by closely related types
- Two related classes that are both a kind of the same broader concept can share the parts of their behavior that are actually identical, and override only the parts that differ.
- Polymorphism replaces long type-checking chains with a single method call
- Code that calls a shared method name on any object of a related family does not need an if/elif chain checking which specific type it is.
- Many frameworks expect you to extend a base class they provide
- Web frameworks' view classes and testing frameworks' test-case classes are commonly used by subclassing them and overriding specific methods.
- Overusing inheritance is a well-known, real design mistake
- Deep inheritance chains built for convenience rather than a genuine is-a relationship tend to become fragile exactly when they most need to change.
Inheritance: extending a class
A subclass is declared with the parent class in parentheses, and it automatically gets every attribute and method the parent defines. Overriding a method means defining a method with the same name in the subclass; when it is called on a subclass instance, the subclass's version runs instead of the parent's. Inside an overriding method, super() gives access to the parent's version, which is how a subclass extends behavior instead of fully replacing it - most commonly to call the parent's __init__ so the base attributes still get set up.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f'{self.name} makes a sound'
class Dog(Animal):
def speak(self):
return f'{self.name} barks'
print(Dog('Rex').speak()) # Rex barksPolymorphism: the same call, different behavior
Because Dog and Animal both define speak, code that only knows it has some kind of Animal can call .speak() on it and get the right behavior for whatever the actual object is, without ever checking its exact type. This is what lets a single loop process a list containing several different subclasses uniformly - the calling code stays generic, and each object supplies its own type-appropriate behavior.
class Cat(Animal):
def speak(self):
return f'{self.name} meows'
for animal in [Dog('Rex'), Cat('Milo')]:
print(animal.speak())
# Rex barks
# Milo meowsInheritance vs composition
Inheritance models an is-a relationship: a dog genuinely is a kind of animal. Composition models a has-a relationship: a car has an engine, rather than a car being a kind of engine. A common and well-earned piece of design guidance is to prefer composition over inheritance when the relationship is not truly is-a - reusing a class's method by inheriting from it just to get that one behavior tends to produce a hierarchy that does not reflect reality and becomes awkward to change later. Neither approach is universally correct; the honest test is whether the relationship you are modeling is actually a type-of relationship or a has-a-part relationship.
Mistakes people make here
- Building deep inheritance chains that become fragile
- A change to a base class ripples through every subclass beneath it, often in ways that are not obvious from reading any single subclass. This is sometimes called the fragile base class problem, and it gets worse the deeper the chain runs.
- Inheriting from a class purely to reuse one method, without a genuine is-a relationship
- Inheriting from an unrelated class just to borrow one piece of behavior usually produces a confusing hierarchy. Composition, holding that class as an attribute and calling its method, expresses the same reuse without the false is-a claim.
- Forgetting to call the parent's __init__ when overriding it
- If a subclass defines its own __init__ without calling super().__init__(), any setup the parent class's constructor was responsible for silently never happens, which tends to surface later as a missing attribute rather than an obvious error at construction time.
- Assuming a subclass must override every method it inherits
- Inheriting a method and not overriding it is completely normal - it means the subclass is satisfied with the parent's version. Overriding is something a subclass may do where its behavior genuinely differs, not an obligation.
Strengths and trade-offs
Where it is strong
- Removes duplicate implementations of behavior genuinely shared by related types.
- Lets calling code stay generic - it calls one method name and does not need a type check for every case it might encounter.
- Makes an intentional relationship between concepts explicit in the code, not just in a comment.
The trade-offs
- Deep hierarchies are notoriously hard to change safely, since a base class edit can affect every subclass in ways that are not visible from any one of them.
- It is easy to reach for inheritance when composition - one object simply holding a reference to another - would be simpler and easier to change later.
- Tracing which method actually runs for a given call requires following the class hierarchy, which is one more step than reading a single function body directly.
Who needs this
Anyone working in an OOP codebase in Python, Java, C#, or similar languages, and anyone who needs to extend a framework's base classes to use it as intended. It matters less in codebases that deliberately favor plain functions and data over class hierarchies.
Questions about inheritance and polymorphism
- What's the practical difference between inheritance and composition?
- Inheritance says this class is a specialized version of that class and automatically gets its interface. Composition says this class has one of those as a part and calls into it explicitly. When in doubt, composition is usually the safer default, because it does not commit you to an is-a claim you might later find was not quite true.
- Do I always need to call super().__init__()?
- Only if the subclass defines its own __init__ and still needs whatever the parent's __init__ sets up. If a subclass does not define __init__ at all, the parent's version runs automatically with no extra step needed.
- What is duck typing, and how does it relate to polymorphism?
- Duck typing is Python's looser version of polymorphism: code that calls a method on an object does not actually require that object to inherit from a common base class - it only requires the object to have that method. Inheritance is one way to guarantee that; duck typing means it is not the only way.
- Can a Python class inherit from more than one class?
- Yes, Python supports multiple inheritance directly. It works, but it introduces real complexity around method resolution order, which parent's version of a method wins when more than one defines it, and is worth using deliberately rather than by accident.