Section 4: Inheritance
Start With an Everyday Picture: Job Roles at a Company
Think about job titles at a trading firm. Every Employee has some things in common — a name, an employee ID, a salary, the ability to clock in. But a Trader is a specific kind of employee who also places trades and has a risk limit. An Analyst is also a specific kind of employee, but instead has research coverage areas and writes reports.
Notice the pattern: a Trader is an Employee — with extra stuff added on top. You wouldn't redefine "name, employee ID, salary" from scratch for Trader, Analyst, and every other role — you'd define those once on Employee, and let every role inherit them automatically.
That's inheritance: defining shared structure and behavior once, in a general class, and letting more specific classes automatically get all of it — while adding or changing only what's actually different about them.
Inheritance means a new class can say "I'm basically like this other class, plus a few extra things" — instead of copy-pasting all the shared code again.
Technical: Inheritance is a mechanism where a new class (the subclass / derived class) acquires the fields and methods of an existing class (the superclass / base class), and can add new fields/methods or override existing ones, without modifying the original class.
Why Bother — What Breaks Without It?
Imagine you don't use inheritance. You write MarketOrder with ticker, quantity, timestamp, and a validate() method. Then you write LimitOrder — but since there's no inheritance, you copy-paste ticker, quantity, timestamp, and validate() into it too. Then StopLossOrder — copy-paste again.
Now imagine a bug is found in validate() — say, it doesn't reject a negative quantity. You now have to find and fix that bug in three separate places, and hope you don't miss one. This is the exact same "change is expensive" problem from Section 0, just showing up between classes instead of within one class.
With inheritance: validate() lives once, in a shared Order base class. Fix it once, every subclass gets the fix automatically.
The Code — Order Hierarchy (Stock/Trading Example)
Python:
class Order:
def __init__(self, ticker, quantity):
self.ticker = ticker
self.quantity = quantity
def validate(self):
if self.quantity <= 0:
raise ValueError("Quantity must be positive")
def execute(self):
print("Generic order executed") # a default, can be overridden
class MarketOrder(Order): # MarketOrder IS-A Order
def execute(self): # overriding the base version
self.validate()
print(f"Market order executed for {self.quantity} shares of {self.ticker}")
class LimitOrder(Order):
def __init__(self, ticker, quantity, limit_price):
super().__init__(ticker, quantity) # reuse the parent's constructor
self.limit_price = limit_price
def execute(self):
self.validate()
print(f"Limit order executed for {self.quantity} shares of {self.ticker} at {self.limit_price}")
m = MarketOrder("AAPL", 10)
m.execute() # uses validate() from Order — never redefined it here!
C++:
class Order {
protected:
std::string ticker;
int quantity;
public:
Order(std::string t, int q) : ticker(t), quantity(q) {}
void validate() {
if (quantity <= 0) {
throw std::invalid_argument("Quantity must be positive");
}
}
virtual void execute() {
std::cout << "Generic order executed" << std::endl;
}
virtual ~Order() {}
};
class MarketOrder : public Order { // MarketOrder IS-A Order
public:
MarketOrder(std::string t, int q) : Order(t, q) {}
void execute() override {
validate();
std::cout << "Market order executed for " << quantity << " shares of " << ticker << std::endl;
}
};
class LimitOrder : public Order {
private:
double limitPrice;
public:
LimitOrder(std::string t, int q, double lp) : Order(t, q), limitPrice(lp) {}
void execute() override {
validate();
std::cout << "Limit order executed for " << quantity << " shares of " << ticker
<< " at " << limitPrice << std::endl;
}
};
Second Example — Account Hierarchy (Bank Example)
A SavingsAccount and a CurrentAccount both need a balance, a withdraw method, a deposit method — but a SavingsAccount also earns interest, and a CurrentAccount allows going negative up to an overdraft limit.
Python:
class Account:
def __init__(self, balance=0):
self.__balance = balance
def deposit(self, amount):
self.__balance += amount
def get_balance(self):
return self.__balance
class SavingsAccount(Account):
def __init__(self, balance=0, interest_rate=0.03):
super().__init__(balance)
self.interest_rate = interest_rate
def add_interest(self):
interest = self.get_balance() * self.interest_rate
self.deposit(interest)
SavingsAccount never redefined deposit() or get_balance() — it just got them, for free, by inheriting from Account. It only added what's actually new: interest_rate and add_interest().
Key Vocabulary, Defined Plainly
| Term | Meaning |
|---|---|
| Base class / Superclass / Parent class | The general class being inherited from (Order, Account) |
| Derived class / Subclass / Child class | The specific class doing the inheriting (MarketOrder, SavingsAccount) |
| IS-A relationship | The test for "should I use inheritance?" — ask: "is a MarketOrder truthfully a kind of Order?" Yes → inheritance fits. If the answer feels forced, it probably isn't a true IS-A. |
| Overriding | A subclass provides its own version of a method the parent already defines (execute() in both MarketOrder and LimitOrder) |
super() / Order(t, q) | A way for the child's constructor to call the parent's constructor, so shared setup logic isn't duplicated |
Connecting Back to Abstraction (Type 2) — How They Work Together
You've already seen Order used two ways: in Section 3 as an ABC/pure-virtual interface (no shared code, just a contract), and now as a base class with actual shared code (validate(), real fields). Both are real, common patterns:
- Interface (abstract class, no implementation): use when subclasses genuinely have nothing in common except "they must be able to do X."
- Base class with inheritance (shared code): use when subclasses share real, reusable logic (like
validate()), plus each needs to override some specific behavior (execute()).
In practice, a base class can do both at once — provide real shared methods (validate()) and declare some methods as needing to be overridden, which is exactly what Order did above.
Line-by-line syntax differences:
| What | Python | C++ | Why different |
|---|---|---|---|
| Declaring inheritance | class MarketOrder(Order): | class MarketOrder : public Order { | Both declare "IS-A." C++ requires the public keyword (there's also private/protected inheritance — rare, skip for now — public is what you'll use ~99% of the time). |
| Calling the parent's constructor | super().__init__(ticker, quantity) | Order(t, q) in the initializer list | Python uses the super() function. C++ calls the parent constructor directly by name, in the child constructor's initializer list. |
| Overriding a method | Just redefine the method with the same name — no keyword needed | void execute() override { — override keyword recommended | Python has no compiler to check you actually overrode something real. C++'s override catches typos — if you misspell the parent method's name, the compiler errors instead of silently creating an unrelated new method. |
| Fields accessible to subclasses but not outside | N/A (Python doesn't truly enforce this — convention only, e.g. single underscore _ticker) | protected: — a third access level between private and public | C++ has an explicit protected keyword: accessible inside the class and its subclasses, but not from outside code. Python has no compiler-enforced equivalent. |
In your own words — what's the IS-A test, and why does inheritance make bug-fixing cheaper? Use the validate() example.