AI Computer Institute
Expert-curated CS & AI curriculum aligned to CBSE standards. A bharath.ai initiative. About Us

Inheritance and Polymorphism Deep Dive

📚 Programming & Coding⏱️ 20 min read🎓 Grade 9
✍️ AI Computer Institute Editorial Team Updated: August 2026 CBSE-aligned · Peer-reviewed · 20 min read
Content curated by subject matter experts with IIT/NIT backgrounds. All chapters are fact-checked against official CBSE/NCERT syllabi.

The problem that inheritance was invented to solve

Suppose you are building the backend logic for a train-ticket booking tool, similar in spirit to IRCTC. You start with a General ticket class, because that is the simplest fare to calculate.

class GeneralTicket:
    def __init__(self, passenger_name, distance_km):
        self.passenger_name = passenger_name
        self.distance_km = distance_km

    def calculate_fare(self):
        return self.distance_km * 0.5   # base rate: Rs 0.50 per km

    def __str__(self):
        return f"{self.passenger_name}: Rs {self.calculate_fare():.2f}"

It works. Now your teacher asks you to add Sleeper class tickets, which cost the same base rate plus a flat Rs 20 reservation charge. The fastest way to get something working is to copy the whole class and change one line:

class SleeperTicket:
    def __init__(self, passenger_name, distance_km):
        self.passenger_name = passenger_name
        self.distance_km = distance_km

    def calculate_fare(self):
        return self.distance_km * 0.5 + 20

    def __str__(self):
        return f"{self.passenger_name}: Rs {self.calculate_fare():.2f}"

Then AC class tickets arrive — same idea, another full copy, another small change. Three classes now exist, and roughly 80% of each one is identical text: the same __init__, the same __str__, the same attribute names. This is not a hypothetical annoyance. If the railway later revises the base rate from Rs 0.50 to Rs 0.60 per km, you must find and edit that number in three separate places. Miss one, and General tickets and AC tickets silently disagree about what a kilometre costs — a bug that has nothing to do with logic and everything to do with duplicated code drifting out of sync. This exact situation — several classes that are clearly variations of one underlying idea, forced to repeat themselves — is precisely the problem inheritance was designed to solve.

The core idea: describing "is-a" relationships in code

Before touching syntax, notice the relationship between these three ticket types in plain English. A Sleeper ticket is a ticket, with an extra reservation charge. An AC ticket is a ticket, with a different, higher pricing rule. This "is-a" phrasing is the test that tells you inheritance is the right tool. If you can honestly say "X is a Y, but with some differences," X should inherit from Y. If the relationship is really "X uses a Y" or "X contains a Y" (for example, a Booking that contains a Ticket), that is a different design — not inheritance.

Once you have identified an is-a relationship, the fix is: write the shared code exactly once, in a class called the base class (also called the parent class or superclass). Then write small derived classes (also called child classes or subclasses) that state only what is different about them. Everything shared is inherited automatically — you never retype it.

Building the base class

Rewrite the shared logic once, as a plain, self-contained class named Ticket:

class Ticket:
    def __init__(self, passenger_name, distance_km):
        self.passenger_name = passenger_name
        self.distance_km = distance_km

    def calculate_fare(self):
        return self.distance_km * 0.5

    def __str__(self):
        return f"{self.passenger_name}: Rs {self.calculate_fare():.2f}"

This Ticket class is now the single source of truth for "how a ticket stores a passenger's name and distance" and "what the cheapest possible fare looks like."

Creating a child class: the inheritance syntax

To make SleeperTicket a proper child of Ticket instead of a copy of it, Python's syntax is to put the parent class name in parentheses after the child's name:

class SleeperTicket(Ticket):
    def calculate_fare(self):
        base_fare = super().calculate_fare()
        return base_fare + 20

Two things happened here, and both matter. First, class SleeperTicket(Ticket): declares that SleeperTicket inherits everything Ticket has — its __init__, its __str__, its attributes — without a single line of that code being rewritten. Second, inside the new calculate_fare, the call super().calculate_fare() reaches up to the parent class and runs its version of the method, so the child does not have to know or repeat the formula distance_km * 0.5. It just says "start from whatever the parent charges, then add Rs 20 for reservation." ACTicket follows the identical pattern with a different formula:

class ACTicket(Ticket):
    def calculate_fare(self):
        base_fare = super().calculate_fare()
        return base_fare * 3 + 50

Notice what is not in SleeperTicket or ACTicket: no __init__, no self.passenger_name, no __str__. None of that needs to be written again — it is inherited.

Tracing what actually happens when you create an object

A very common misunderstanding is to imagine that when Python "sees" class SleeperTicket(Ticket):, it secretly copies all of Ticket's code into SleeperTicket, as if a hidden paste operation happened. That is not what occurs, and the difference matters for predicting behaviour correctly. Trace this line by line:

t = SleeperTicket("Arjun", 100)

Python creates a bare SleeperTicket object, then needs to run __init__ on it. It looks for __init__ directly inside the SleeperTicket class body — there isn't one. So Python walks up to the parent, Ticket, finds __init__ there, and runs that, setting t.passenger_name = "Arjun" and t.distance_km = 100. No code was copied; Python simply kept looking upward along the class chain until it found a matching method. This search path is called the Method Resolution Order (MRO): child class first, then parent, then grandparent, and so on, stopping at the first match. Every attribute and method lookup in Python OOP works this way, every single time — it is a live search, not a one-time copy, which is why changing the parent class later instantly affects every child that has not overridden that particular piece.

Method overriding: giving your own version

Now call t.calculate_fare() on that same object. The MRO search starts at SleeperTicket — and this time it finds a match immediately, because SleeperTicket defines its own calculate_fare. Python stops searching right there; the parent's version is never even consulted, except for the explicit super().calculate_fare() call written inside the child's own method. This is called method overriding: a child class defining a method with the exact same name as one in its parent, so that the child's version takes priority.

Trace the full numeric computation for distance_km = 100 across all three classes:

  • Ticket.calculate_fare()100 * 0.550.0
  • SleeperTicket.calculate_fare()super().calculate_fare() returns 50.0, then 50.0 + 2070.0
  • ACTicket.calculate_fare()super().calculate_fare() returns 50.0, then 50.0 * 3 + 50200.0

Every one of those calculations reused the parent's base-rate logic instead of retyping distance_km * 0.5. That is the entire payoff of inheritance: change the base rate once inside Ticket, and SleeperTicket and ACTicket automatically pick up the new value the very next time super().calculate_fare() runs, because — as established above — the lookup happens live, not from a frozen copy.

Common misconception: overriding is not overloading

Students coming from math class or from hearing about C++/Java often expect Python to support method overloading — defining the same method name multiple times with different parameter lists, and having Python automatically pick the right one based on how many arguments you pass. Python does not work this way. Trace this carefully:

class Demo:
    def greet(self):
        print("Hi")
    def greet(self, name):
        print("Hi", name)

d = Demo()
d.greet()

A class body is executed top to bottom like ordinary code. The first def greet(self): creates a function object and stores it under the name "greet" in the class's namespace. The second def greet(self, name): does not add a second option — it simply overwrites the same name with a new function object, exactly the way x = 1 followed by x = 2 leaves only 2. By the time the class body finishes, Demo.greet refers only to the two-argument version. So d.greet() raises TypeError: greet() missing 1 required positional argument: 'name' — Python is not confused about which version to run; there was only ever one version left standing. Overriding (a child class replacing a parent's method) and overloading (multiple versions of one method chosen by argument count/type) are different mechanisms, and only the first one is a real feature of Python classes.

Multilevel inheritance

Inheritance chains do not have to stop at one level. Suppose Tatkal (last-minute) AC tickets need everything an AC ticket has, plus a Rs 100 premium. Since "TatkalACTicket is-a ACTicket," it should inherit from ACTicket, not from Ticket directly:

class TatkalACTicket(ACTicket):
    def calculate_fare(self):
        ac_fare = super().calculate_fare()
        return ac_fare + 100

Trace it for distance_km = 100: super().calculate_fare() inside TatkalACTicket follows the MRO to the very next class in line, which is ACTicket — not Ticket. ACTicket.calculate_fare() itself calls its super(), reaching Ticket, giving 50.0, then computing 50.0 * 3 + 50 = 200.0. Back in TatkalACTicket, that 200.0 becomes 200.0 + 100 = 300.0. Three classes, three separate overrides, each one only responsible for its own small adjustment — this layering is called multilevel inheritance, and it is exactly how real class libraries stay manageable as they grow: nobody is forced to re-derive the whole formula from scratch at every level.

What polymorphism actually means

"Poly" means many, "morph" means form — polymorphism is the ability of one piece of code, calling one method name in one place, to automatically produce different behaviour depending on which object it is actually operating on. It sounds abstract, but you already built the mechanism that makes it work: method overriding plus the MRO's live lookup. Watch it happen with a single loop over a list of different ticket types:

tickets = [
    Ticket("Meera", 100),
    SleeperTicket("Arjun", 100),
    ACTicket("Divya", 100),
    TatkalACTicket("Kabir", 100),
]

for t in tickets:
    print(t)

Output:

Meera: Rs 50.00
Arjun: Rs 70.00
Divya: Rs 200.00
Kabir: Rs 300.00

Look closely at what the loop body actually contains: print(t), one line, written once. Internally that calls t.__str__(), and __str__ is defined only in the base Ticket class — none of the child classes overrode it. Yet inside that single shared __str__, the line self.calculate_fare() produces four different numbers across the four iterations. This is the precise mechanism of polymorphism: self always refers to the actual object sitting in memory, and Python always looks up calculate_fare starting from that object's real class, following the MRO from there — never from wherever the calling code happens to live. The __str__ method, written entirely inside Ticket, has no idea it might be running on behalf of a TatkalACTicket; it simply asks "whatever self is, calculate its fare," and the correct override answers every time. That is why the same call site produces four different results without a single if statement checking "what type of ticket is this."

Common misconception: polymorphism is not "many unrelated behaviours"

It is tempting to think polymorphism just means "different classes can do different things," but that description is too loose — of course unrelated classes behave differently; that is not a special concept. The precise meaning is narrower and more useful: the same method name, called through the same interface, resolves to different, deliberately related implementations depending on the object's actual class. If SleeperTicket instead had a method called sleeper_price() while ACTicket had a differently named ac_price(), calling code would need a separate if branch for every ticket type, and the loop above would break. Polymorphism specifically means designers agreed on one shared method name (calculate_fare) across the whole family, so that calling code can stay generic and single-branch, while each class privately supplies its own correct arithmetic behind that shared name.

Polymorphism beyond your own classes: it is everywhere in Python

This same idea is not limited to classes you design yourself — it is baked into Python's built-in functions. Call len() on a string, a list, and a dictionary:

print(len("Namaste"))
print(len([10, 20, 30, 40]))
print(len({"UPI": 1, "IMPS": 2}))

Output: 7, 4, 2. One function name, three completely different internal counting rules — character count, element count, key count — chosen automatically based on the actual type of the argument. Python programmers call this duck typing ("if it walks like a duck and quacks like a duck..."): len() does not check "are you officially a string class"; it simply asks the object to report its own length, the same spirit as calculate_fare() asking each ticket to report its own fare.

Multiple inheritance, briefly

Python also allows a class to inherit from more than one parent at once, by listing several class names in the parentheses. This is useful for adding an unrelated capability to a class, rather than describing a strict is-a hierarchy:

class GPSTrackable:
    def track_location(self):
        return "Location shared with control room"

class PremiumTicket(ACTicket, GPSTrackable):
    pass

p = PremiumTicket("Rohan", 100)
print(p.calculate_fare())
print(p.track_location())

Output: 200.0 and Location shared with control room. PremiumTicket has no body of its own beyond pass, yet it works fully, because the MRO now searches across both parents: calculate_fare is found in ACTicket (which in turn reaches Ticket via its own super()), while track_location is found in GPSTrackable. Notice the difference in relationship: "PremiumTicket is-a ACTicket" is true and meaningful, but "PremiumTicket is-a GPSTrackable" reads oddly — a ticket is not really "a trackable." GPSTrackable here is closer to a bolt-on capability (often called a mixin) than a true ancestor. Multiple inheritance is powerful precisely because it lets you combine "what kind of thing is this" (via a normal is-a parent) with "what extra abilities does it have" (via mixins), but it is worth using sparingly — when two parent classes define a method with the exact same name, the MRO's left-to-right rule decides the winner, and untangling that for more than two or three parents gets confusing fast.

Types of inheritance Python supports, at a glance

  • Single inheritance — one child, one parent (SleeperTicket(Ticket)).
  • Multilevel inheritance — a chain of three or more classes (TatkalACTicket → ACTicket → Ticket).
  • Hierarchical inheritance — several children sharing one parent (SleeperTicket and ACTicket both extend Ticket).
  • Multiple inheritance — one child, two or more parents (PremiumTicket(ACTicket, GPSTrackable)).

The full picture

Ticket class hierarchy — single, hierarchical & multilevel inheritance Ticket (base class) calculate_fare() = distance_km × 0.5 SleeperTicket calculate_fare() = super() + 20 ACTicket calculate_fare() = super() × 3 + 50 TatkalACTicket calculate_fare() = super() + 100 is-a is-a is-a Polymorphism: same call, resolved differently per object Meera (Ticket) t.calculate_fare() → ₹50.00 Arjun (SleeperTicket) t.calculate_fare() → ₹70.00 Divya (ACTicket) t.calculate_fare() → ₹200.00 Kabir (TatkalACTicket) t.calculate_fare() → ₹300.00 Same code, same method name — Python looks up calculate_fare() on the object's real class, not on the variable's name.

Active recall

  1. Trace this by hand before running it: a new class SuperTatkalACTicket(TatkalACTicket) adds a Rs 50 "instant confirmation" fee on top of whatever TatkalACTicket charges. For distance_km = 100, what number does calculate_fare() return? Show every super() hop.
  2. A classmate writes class ACTicket: instead of class ACTicket(Ticket): by mistake, but keeps super().calculate_fare() inside the method. What actually goes wrong when the code runs, and why?
  3. Explain in your own words why the loop for t in tickets: print(t) is an example of polymorphism, but a version of the same loop with if isinstance(t, SleeperTicket): ... elif isinstance(t, ACTicket): ... inside it would defeat the purpose of writing calculate_fare() as an override in the first place.
  4. Is "a Bowler is a Player" a good candidate for inheritance? Is "a Player has a Jersey" a good candidate for inheritance? Justify each answer using the is-a test from this chapter.
  5. Without running it, predict the exact printed output of the Demo.greet example from the misconception section, and name the built-in Python mechanism (not "a bug") responsible for that output.

Summary

Inheritance lets a child class reuse everything defined in its parent class — attributes, methods, everything — without retyping it, by declaring the relationship with class Child(Parent):. Attribute and method lookups are not copies made once; they are live searches that walk the Method Resolution Order from the object's real class upward until a match is found, which is exactly why editing a parent's method instantly changes behaviour for every child that has not overridden it. A child can supply its own version of a method — overriding — and still reach the parent's original version explicitly through super(), which is how SleeperTicket, ACTicket, and the multilevel TatkalACTicket all built on one shared fare formula instead of repeating it. Overriding is not the same as overloading: Python keeps only the last definition of a method name written in a class body. Polymorphism is the direct consequence of overriding combined with live MRO lookup: one call site, one method name, automatically producing the correct behaviour for whatever object is actually there at runtime — the same principle that lets Python's own len() work correctly on a string, a list, or a dictionary. Python additionally supports multiple inheritance, letting a class combine a true is-a parent with unrelated mixin capabilities, resolved by the same left-to-right MRO rule.

Practice Exercises

Now it is time to practice! Complete these challenges to solidify your understanding:

  • Exercise 1: Write a short program that demonstrates the core concept from this chapter. Test it with at least 3 different inputs.
  • Exercise 2: Find a real-world example where inheritance and polymorphism deep dive is used in an Indian company (like TCS, Infosys, Flipkart, or ISRO). Write a paragraph explaining the connection.
  • Exercise 3: Create a mind-map connecting inheritance and polymorphism deep dive to at least 3 other topics you have studied.
← Backpropagation: How Neural Networks LearnDesign Patterns: Factory, Singleton, Observer →

Found this useful? Share it!

📱 WhatsApp 🐦 Twitter 💼 LinkedIn