Python · Intermediate

Inheritance in Python

10 min readUpdated September 24, 2026Every example verified

In short: Inheritance lets one class (the subclass) reuse the attributes and methods of another (the base class) and change or extend them. Write class Child(Parent):, override a method by redefining it with the same name, and call super() inside the override to reuse the parent's version.

What inheritance does and how lookup works

When two classes are mostly the same, inheritance lets you write the shared part once. A StudentMembership is a Membership with a discount; an AuditedAccount is an Account that also flags large deposits. The subclass names its parent in parentheses, class StudentMembership(Membership):, and immediately has every attribute and method the parent defines. It then adds what is new and overrides what must differ by defining a method with the same name.

The rule for lookup is simple: Python searches the object's own class first, then its parent, then the parent's parent, up to object, the root of every class. That chain is the method resolution order, visible as Class.__mro__. The first definition found wins, which is what makes overriding work: StudentMembership.yearly_cost shadows Membership.yearly_cost for student objects only, and a base-class method that calls self.yearly_cost() automatically picks up the override.

Overriding usually means extending rather than replacing. super() returns a proxy that continues the lookup from the parent, so super().yearly_cost() runs the base calculation and the subclass adjusts the result. The same pattern applies to __init__: a subclass that adds attributes should call super().__init__(...) first so the parent's attributes exist, then set its own. Forgetting that call is the most common inheritance bug, because the object looks fine until some inherited method reads an attribute that was never created.

The payoff is polymorphism: code written for the base type works with every subclass without checking which one it has. A loop over Notifier objects calls send on each, and an email notifier and an SMS notifier respond in their own way. New kinds can be added later without touching the loop. When the base class cannot sensibly implement a method, it can raise NotImplementedError so a subclass that forgets to override fails clearly; the abc module makes such requirements formal.

isinstance(obj, Base) is True for instances of Base and of any subclass, which is almost always what a type check should mean. type(obj) is Base is exact and excludes subclasses, so use it only when that distinction is the point. issubclass(Child, Base) answers the same question about classes rather than objects.

Inheritance models an "is a" relationship. When the relationship is "has a", such as a car that has an engine, put an engine object inside the car instead: that is composition, and it keeps the classes independent. Deep hierarchies where every class overrides a little of its parent are hard to reason about; two levels are usually enough.

Syntax

 Python · syntax
class Membership:
    def __init__(self, name, fee):
        self.name = name
        self.fee = fee

    def yearly_cost(self):
        return self.fee * 12

class StudentMembership(Membership):          # subclass of Membership
    def __init__(self, name, fee, discount):
        super().__init__(name, fee)           # parent's __init__ first
        self.discount = discount

    def yearly_cost(self):                    # override, reusing the parent
        return super().yearly_cost() * (1 - self.discount)

isinstance(obj, Membership)                   # True for subclass instances too
issubclass(StudentMembership, Membership)     # True

A subclass that defines no __init__ inherits the parent's, so StudentMembership only needs its own because it adds a parameter.

Extending a method and checking types

An audited account adds a check before deposits but keeps the parent's bookkeeping. The last lines compare isinstance with an exact type test.

 Python
class Account:
    def __init__(self, owner):
        self.owner = owner
        self.balance = 0
        self.history = []

    def deposit(self, amount):
        self.balance += amount
        self.history.append(("deposit", amount))

class AuditedAccount(Account):
    def deposit(self, amount):
        if amount > 1000:
            print(f"flagged: large deposit of {amount}")
        super().deposit(amount)

plain = Account("Rosa")
audited = AuditedAccount("Rosa")
for acct in (plain, audited):
    acct.deposit(500)
    acct.deposit(2500)
    print(acct.balance, len(acct.history))

print(isinstance(audited, Account), type(audited) is Account)
print(type(audited).__name__)

Output

3000 2
flagged: large deposit of 2500
3000 2
True False
AuditedAccount

AuditedAccount defines no __init__, so the parent's runs and every audited account still gets balance and history. Its deposit adds the check and then delegates to super().deposit, so the balance and history end up identical to the plain account's; only the flag message differs. isinstance says an audited account is an Account, while the exact type test says it is not, which is why isinstance is the right tool for most checks.

Inheritance or composition?

Question to askRelationshipUse
Is a StudentMembership a Membership?is ainheritance: class StudentMembership(Membership)
Does a Car have an Engine?has acomposition: self.engine = Engine()
Do several unrelated classes need one extra ability?can doa mixin class or a plain function

Common mistakes

  • Not calling super().__init__() in the subclass

    Why it goes wrong: Once you define your own __init__, the parent's is not run automatically, so the attributes it would have set never exist and inherited methods fail with AttributeError.

    Fix: Call super().__init__(...) with the parent's arguments before setting the subclass's own attributes.

     Python · fix
    def __init__(self, name, fee, discount):
        super().__init__(name, fee)
        self.discount = discount
  • Overriding a method with a different signature

    Why it goes wrong: Code written for the base class calls send(text); a subclass whose send needs an extra required argument breaks that code with TypeError, which defeats the purpose of sharing a base.

    Fix: Keep the same parameters, and give any additions a default value.

  • Checking type(obj) == Base to accept subclasses

    Why it goes wrong: type() returns the exact class, so subclass instances fail the check even though they support everything the base does.

    Fix: Use isinstance(obj, Base), or better, just call the method and let polymorphism work.

  • Inheriting to reuse code when it is not an is-a relationship

    Why it goes wrong: A Report(list) that subclasses list to reuse append inherits dozens of other methods that make no sense for a report.

    Fix: Hold the list as an attribute and expose only the operations a report needs.

Where you use this

A base class with a few subclasses is the natural shape for "the same job done several ways". A backup tool can define Storage with save and load, then LocalStorage, SftpStorage and CloudStorage that implement them; the rest of the program is written once against Storage.

Frameworks use inheritance to let you plug in behaviour: a web framework's view class, a test runner's test-case class or the standard library's Exception are base classes you subclass to add your own specifics while inheriting the machinery. Defining class InsufficientStock(Exception) in the exceptions lesson was already inheritance at work.

 Python · in practice
class Storage:
    def save(self, name, data):
        raise NotImplementedError

class LocalStorage(Storage):
    def save(self, name, data):
        with open(name, "w", encoding="utf-8") as f:
            f.write(data)

Key points

  • class Child(Parent): gives Child every attribute and method of Parent.
  • Redefining a method in the subclass overrides it; super() reaches the parent's version.
  • Call super().__init__(...) in a subclass __init__ before adding attributes.
  • Method lookup follows the MRO: the object's class, then each parent in order, then object.
  • isinstance includes subclasses; type(obj) is Cls is exact.
  • Prefer composition for "has a" relationships and keep hierarchies shallow.

Practise this

Exercises for this lesson are in the Python practice set.

Open the Python playground

Frequently asked questions

What does super() do in Python?

super() returns a proxy object that looks up attributes starting from the next class in the method resolution order after the current one. Inside a subclass method, super().method() calls the parent's implementation, which lets an override extend behaviour instead of copying it. In __init__ it is how the subclass runs the parent's initialisation before adding its own attributes.

Does Python support multiple inheritance?

Yes: class C(A, B): inherits from both, and the method resolution order (computed by the C3 linearisation algorithm) decides which definition wins when both parents define the same name, searching left to right. It is mostly used for mixins, small classes that add one capability. Single inheritance plus composition covers the vast majority of programs and is easier to reason about.

What is the difference between isinstance and type?

isinstance(obj, Cls) is True if obj is an instance of Cls or of any subclass, which respects the is-a relationship that inheritance creates. type(obj) is Cls is True only for the exact class. Use isinstance for ordinary type checks and type only when subclasses must be excluded deliberately.

Progress is stored only in this browser.

How this page was checked. Every program on it was run with CPython 3.11 at build time; runs in your browser on CPython 3.14 (Pyodide) by the publishing checks, and the output shown is what it printed. The Run buttons execute the same code on your device; nothing you type is sent anywhere.