Python Inheritance and super(): Build New Classes from Old Ones
Priya starts copying an Order class into DiscountedOrder. Kai stops her cold. Inheritance: don't copy the blueprint, extend it.
Okay so I have this Order class. Works great. But now I need a DiscountedOrder that has everything Order has, plus a discount percentage and a different total calculation. I'm going to copy the whole class and modify it.
Stop. Don't copy the class.
But I need the same fields — id, customer, total. If I don't copy, how do I get them?
You inherit them. You tell Python: "DiscountedOrder is a type of Order. Give me everything Order has. I'm just adding one extra thing."
Inheritance. Like, the OOP thing. I've read about it but I thought it was for giant frameworks, not my code.
You're sitting on a building with an old blueprint. You don't demolish it and reprint the blueprint. You take the existing blueprint and write: "Extend the foundation for three more rooms." Same building code applies. New additions follow the same rules.
So the syntax is...
class DiscountedOrder(Order): — the parentheses tell Python that DiscountedOrder inherits from Order. Here:
class Order:
def __init__(self, id, customer, total):
self.id = id
self.customer = customer
self.total = total
def get_summary(self):
return f"Order {self.id}: {self.customer}, ${self.total:.2f}"
class DiscountedOrder(Order):
def __init__(self, id, customer, total, discount_pct):
super().__init__(id, customer, total)
self.discount_pct = discount_pct
What's super().__init__()? That looks like magic.
It's not magic. It's a function call. super() means "the parent class." super().__init__() means "run the parent's init method." You pass it the arguments that Order's init needs.
So I'm calling Order's __init__ from inside DiscountedOrder's init? Why?
Because Order's __init__ sets up self.id, self.customer, and self.total. If you don't call super().__init__(), those attributes never get created. Your DiscountedOrder object would have self.discount_pct but no id, no customer, nothing.
So every time I inherit, I have to call super().__init__() first thing?
Not every time. Only if the parent has an __init__ and you're adding new logic. If your child class doesn't need to do anything special in init, you don't write an __init__ at all — you just use the parent's.
class RegularOrder(Order):
# No __init__ here — inherit Order's completely
pass
order = RegularOrder("ORD-001", "Alice", 99.99)
print(order.id) # works — inherited from Order
But DiscountedOrder does need something extra — the discount percentage. So I write an init, call super().__init__() for the parent setup, then add my own thing.
Exactly. You're extending, not replacing.
Okay what about methods? If I want DiscountedOrder to have a different get_summary() that includes the discount?
You override it — write a new version in the child class:
class DiscountedOrder(Order):
def __init__(self, id, customer, total, discount_pct):
super().__init__(id, customer, total)
self.discount_pct = discount_pct
def get_summary(self):
return f"Order {self.id}: {self.customer}, ${self.total:.2f} minus {self.discount_pct}% discount"
Now when you call get_summary() on a DiscountedOrder, it uses DiscountedOrder's version, not Order's.
But what if I want to keep some of the parent's logic and just add to it?
You call super() again.
class DiscountedOrder(Order):
def __init__(self, id, customer, total, discount_pct):
super().__init__(id, customer, total)
self.discount_pct = discount_pct
def get_summary(self):
# Start with the parent's summary, then add discount info
parent_summary = super().get_summary()
return f"{parent_summary} (discount: {self.discount_pct}%)"
So super() isn't just for __init__. It's "call the parent's version of this method." That's... actually useful. What if Order changes tomorrow and adds a discount_amount field to the summary? DiscountedOrder automatically gets the new field without copying anything.
That's exactly right. You just described the entire point. Maintenance. One copy of the logic.
But I could have just... added a discount_pct parameter to Order's __init__ and made it optional. Made it simpler. Why inherit instead of just modifying the parent?
Fair question. Picture this: six months from now, you have Order, DiscountedOrder, BulkOrder, GiftCardOrder, PreorderOrder — five classes that all need the same base logic.
Scenario 1: You put discount_pct, bulk_quantity, gift_card_amount, preorder_date all in Order's init with defaults.
class Order:
def __init__(self, id, customer, total, discount_pct=None, bulk_quantity=None, ...):
# Now Order's init has eight parameters and half of them are None
# New code has to handle all that machinery even if it doesn't use it
Scenario 2: Each order type inherits from Order, adds only what it needs.
class Order: # Just the essentials
def __init__(self, id, customer, total):
self.id = id
self.customer = customer
self.total = total
class DiscountedOrder(Order): # Order plus discount
def __init__(self, id, customer, total, discount_pct):
super().__init__(id, customer, total)
self.discount_pct = discount_pct
class BulkOrder(Order): # Order plus bulk quantity
def __init__(self, id, customer, total, bulk_quantity):
super().__init__(id, customer, total)
self.bulk_quantity = bulk_quantity
So inheritance keeps the base class clean. New subclasses are small and focused.
And each subclass is a contract: "I am a type of Order. I have everything Order has, plus these specific things." When someone reads isinstance(order, Order), they know it's safe to call get_summary() because every subclass inherits that.
Wait — isinstance still works with subclasses?
Yes. A DiscountedOrder is an Order. Python treats it that way.
order = DiscountedOrder("ORD-101", "Bob", 100.00, 20)
print(isinstance(order, DiscountedOrder)) # True
print(isinstance(order, Order)) # True — because it inherited
print(isinstance(order, str)) # False
Alright but here's a question that's bothering me. When should I use inheritance versus composition? Like, what if DiscountedOrder didn't inherit Order — what if it had an Order inside it?
There's the real question. Inheritance: DiscountedOrder IS-A Order. Composition: DiscountedOrder HAS-A Order.
# Inheritance: IS-A
class DiscountedOrder(Order):
pass
# Composition: HAS-A
class DiscountedOrder:
def __init__(self, order, discount_pct):
self.order = order # DiscountedOrder contains an Order
self.discount_pct = discount_pct
def get_total(self):
return self.order.total * (1 - self.discount_pct / 100)
IS-A: DiscountedOrder is a subtype of Order. It passes isinstance checks. It's a legitimate order in every context.
Composition: DiscountedOrder is a separate thing that wraps an Order. It's a discount calculator, not an order.
So which one do I actually use?
If DiscountedOrder should work everywhere an Order works — process it the same way, store it the same way, send it to the same API — inherit. If it's just using Order's data to compute something different, compose.
In your codebase: billing needs to call get_summary() on any order. Reporting needs to iterate get_total() on any order. Those need inheritance. But if you have a DiscountStrategy object that wraps an order and calculates discounts on the side — that's composition.
So the rule is: inherit when subtype, compose when decoration.
That's going to serve you well.
Is there anything weird about inheritance I should know?
Method resolution order — MRO. If you have multiple inheritance (which you should avoid), Python reads methods from left to right in the parent list. But for single inheritance — one parent — it's straightforward. DiscountedOrder looks for methods in itself first, then in Order, then in object.
Is there anything else?
Private attributes don't exist in Python — any attribute starting with self._ is just "please don't touch this, it's internal." Subclasses can still access them if they want. It's trust, not protection. Just remember it when you override methods.
So inheritance is about saying "this is a type of that." super() makes sure the parent gets set up. Override when you need different behavior. Use composition when you're just wrapping something.
You've got it.
And tomorrow is dataclasses, right? Because typing all these __init__ parameters is getting old.
Tomorrow. You'll write less code, but you'll understand what's happening under the hood in Amir's codebase — because Amir uses dataclasses for every model in the order system. Today you understand inheritance. Tomorrow you get to skip writing the boilerplate.
So I'm learning inheritance today, then tomorrow I'll see that I don't have to write all this init stuff by hand?
Exactly. Patterns first, shortcuts second. That's how it sticks.
Practice your skills
Sign up to write and run code in this lesson.
Python Inheritance and super(): Build New Classes from Old Ones
Priya starts copying an Order class into DiscountedOrder. Kai stops her cold. Inheritance: don't copy the blueprint, extend it.
Okay so I have this Order class. Works great. But now I need a DiscountedOrder that has everything Order has, plus a discount percentage and a different total calculation. I'm going to copy the whole class and modify it.
Stop. Don't copy the class.
But I need the same fields — id, customer, total. If I don't copy, how do I get them?
You inherit them. You tell Python: "DiscountedOrder is a type of Order. Give me everything Order has. I'm just adding one extra thing."
Inheritance. Like, the OOP thing. I've read about it but I thought it was for giant frameworks, not my code.
You're sitting on a building with an old blueprint. You don't demolish it and reprint the blueprint. You take the existing blueprint and write: "Extend the foundation for three more rooms." Same building code applies. New additions follow the same rules.
So the syntax is...
class DiscountedOrder(Order): — the parentheses tell Python that DiscountedOrder inherits from Order. Here:
class Order:
def __init__(self, id, customer, total):
self.id = id
self.customer = customer
self.total = total
def get_summary(self):
return f"Order {self.id}: {self.customer}, ${self.total:.2f}"
class DiscountedOrder(Order):
def __init__(self, id, customer, total, discount_pct):
super().__init__(id, customer, total)
self.discount_pct = discount_pct
What's super().__init__()? That looks like magic.
It's not magic. It's a function call. super() means "the parent class." super().__init__() means "run the parent's init method." You pass it the arguments that Order's init needs.
So I'm calling Order's __init__ from inside DiscountedOrder's init? Why?
Because Order's __init__ sets up self.id, self.customer, and self.total. If you don't call super().__init__(), those attributes never get created. Your DiscountedOrder object would have self.discount_pct but no id, no customer, nothing.
So every time I inherit, I have to call super().__init__() first thing?
Not every time. Only if the parent has an __init__ and you're adding new logic. If your child class doesn't need to do anything special in init, you don't write an __init__ at all — you just use the parent's.
class RegularOrder(Order):
# No __init__ here — inherit Order's completely
pass
order = RegularOrder("ORD-001", "Alice", 99.99)
print(order.id) # works — inherited from Order
But DiscountedOrder does need something extra — the discount percentage. So I write an init, call super().__init__() for the parent setup, then add my own thing.
Exactly. You're extending, not replacing.
Okay what about methods? If I want DiscountedOrder to have a different get_summary() that includes the discount?
You override it — write a new version in the child class:
class DiscountedOrder(Order):
def __init__(self, id, customer, total, discount_pct):
super().__init__(id, customer, total)
self.discount_pct = discount_pct
def get_summary(self):
return f"Order {self.id}: {self.customer}, ${self.total:.2f} minus {self.discount_pct}% discount"
Now when you call get_summary() on a DiscountedOrder, it uses DiscountedOrder's version, not Order's.
But what if I want to keep some of the parent's logic and just add to it?
You call super() again.
class DiscountedOrder(Order):
def __init__(self, id, customer, total, discount_pct):
super().__init__(id, customer, total)
self.discount_pct = discount_pct
def get_summary(self):
# Start with the parent's summary, then add discount info
parent_summary = super().get_summary()
return f"{parent_summary} (discount: {self.discount_pct}%)"
So super() isn't just for __init__. It's "call the parent's version of this method." That's... actually useful. What if Order changes tomorrow and adds a discount_amount field to the summary? DiscountedOrder automatically gets the new field without copying anything.
That's exactly right. You just described the entire point. Maintenance. One copy of the logic.
But I could have just... added a discount_pct parameter to Order's __init__ and made it optional. Made it simpler. Why inherit instead of just modifying the parent?
Fair question. Picture this: six months from now, you have Order, DiscountedOrder, BulkOrder, GiftCardOrder, PreorderOrder — five classes that all need the same base logic.
Scenario 1: You put discount_pct, bulk_quantity, gift_card_amount, preorder_date all in Order's init with defaults.
class Order:
def __init__(self, id, customer, total, discount_pct=None, bulk_quantity=None, ...):
# Now Order's init has eight parameters and half of them are None
# New code has to handle all that machinery even if it doesn't use it
Scenario 2: Each order type inherits from Order, adds only what it needs.
class Order: # Just the essentials
def __init__(self, id, customer, total):
self.id = id
self.customer = customer
self.total = total
class DiscountedOrder(Order): # Order plus discount
def __init__(self, id, customer, total, discount_pct):
super().__init__(id, customer, total)
self.discount_pct = discount_pct
class BulkOrder(Order): # Order plus bulk quantity
def __init__(self, id, customer, total, bulk_quantity):
super().__init__(id, customer, total)
self.bulk_quantity = bulk_quantity
So inheritance keeps the base class clean. New subclasses are small and focused.
And each subclass is a contract: "I am a type of Order. I have everything Order has, plus these specific things." When someone reads isinstance(order, Order), they know it's safe to call get_summary() because every subclass inherits that.
Wait — isinstance still works with subclasses?
Yes. A DiscountedOrder is an Order. Python treats it that way.
order = DiscountedOrder("ORD-101", "Bob", 100.00, 20)
print(isinstance(order, DiscountedOrder)) # True
print(isinstance(order, Order)) # True — because it inherited
print(isinstance(order, str)) # False
Alright but here's a question that's bothering me. When should I use inheritance versus composition? Like, what if DiscountedOrder didn't inherit Order — what if it had an Order inside it?
There's the real question. Inheritance: DiscountedOrder IS-A Order. Composition: DiscountedOrder HAS-A Order.
# Inheritance: IS-A
class DiscountedOrder(Order):
pass
# Composition: HAS-A
class DiscountedOrder:
def __init__(self, order, discount_pct):
self.order = order # DiscountedOrder contains an Order
self.discount_pct = discount_pct
def get_total(self):
return self.order.total * (1 - self.discount_pct / 100)
IS-A: DiscountedOrder is a subtype of Order. It passes isinstance checks. It's a legitimate order in every context.
Composition: DiscountedOrder is a separate thing that wraps an Order. It's a discount calculator, not an order.
So which one do I actually use?
If DiscountedOrder should work everywhere an Order works — process it the same way, store it the same way, send it to the same API — inherit. If it's just using Order's data to compute something different, compose.
In your codebase: billing needs to call get_summary() on any order. Reporting needs to iterate get_total() on any order. Those need inheritance. But if you have a DiscountStrategy object that wraps an order and calculates discounts on the side — that's composition.
So the rule is: inherit when subtype, compose when decoration.
That's going to serve you well.
Is there anything weird about inheritance I should know?
Method resolution order — MRO. If you have multiple inheritance (which you should avoid), Python reads methods from left to right in the parent list. But for single inheritance — one parent — it's straightforward. DiscountedOrder looks for methods in itself first, then in Order, then in object.
Is there anything else?
Private attributes don't exist in Python — any attribute starting with self._ is just "please don't touch this, it's internal." Subclasses can still access them if they want. It's trust, not protection. Just remember it when you override methods.
So inheritance is about saying "this is a type of that." super() makes sure the parent gets set up. Override when you need different behavior. Use composition when you're just wrapping something.
You've got it.
And tomorrow is dataclasses, right? Because typing all these __init__ parameters is getting old.
Tomorrow. You'll write less code, but you'll understand what's happening under the hood in Amir's codebase — because Amir uses dataclasses for every model in the order system. Today you understand inheritance. Tomorrow you get to skip writing the boilerplate.
So I'm learning inheritance today, then tomorrow I'll see that I don't have to write all this init stuff by hand?
Exactly. Patterns first, shortcuts second. That's how it sticks.