Python · Intermediate
Inheritance in Python
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
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) # TrueA 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.
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 ask | Relationship | Use |
|---|---|---|
| Is a StudentMembership a Membership? | is a | inheritance: class StudentMembership(Membership) |
| Does a Car have an Engine? | has a | composition: self.engine = Engine() |
| Do several unrelated classes need one extra ability? | can do | a 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 withAttributeError.Fix: Call
super().__init__(...)with the parent's arguments before setting the subclass's own attributes.Python · fixdef __init__(self, name, fee, discount): super().__init__(name, fee) self.discount = discountOverriding a method with a different signature
Why it goes wrong: Code written for the base class calls
send(text); a subclass whosesendneeds an extra required argument breaks that code withTypeError, 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 subclasseslistto reuseappendinherits 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.
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. isinstanceincludes subclasses;type(obj) is Clsis exact.- Prefer composition for "has a" relationships and keep hierarchies shallow.
Practise this
Exercises for this lesson are in the Python practice set.
Related lessons
- Exceptions and Error Handling in PythonHow Python exceptions work: catching specific errors with try/except, using else and finally, raising your own, and defining custom exception classes.10 min
- Functions in PythonHow to define and call Python functions with def, pass arguments, return results, use default and keyword arguments, and why variables inside are local.9 min
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.
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.