Every sklearn model — LinearRegression, RandomForest, KMeans — answers .fit() and .predict(), and your pipeline code never cares which one it got. That interchangeability isn't a coincidence; it's inheritance: each model IS-A BaseEstimator, sharing one contract while overriding the parts that differ.
What you'll learn
What
Inheritance lets a class (the child/subclass) build on another (the parent/base class): it inherits every attribute and method, OVERRIDES the ones that must differ, and EXTENDS parent behavior with super(). isinstance() respects the family tree.
Why
Inheritance is how frameworks hand you 95% of a solution and let you customize the rest: custom Django models, PyTorch modules, Airflow operators, pytest fixtures — all 'subclass and override one method'. Reading library code without understanding inheritance is impossible; using it well (and knowing when NOT to) is a design skill interviews test directly.
Where it's used
sklearn estimators, PyTorch nn.Module, Django models, exception hierarchies (next module!), and any 'plugin' architecture where variants share a contract.
Where this runs in production
You define class MyNet(nn.Module), call super().__init__(), and override forward() — the framework's training loop runs YOUR forward through ITS machinery. Deep learning code is inheritance.
A data team's custom SlackAlertOperator subclasses BaseOperator and overrides execute() — scheduling, retries, and logging come free from the parent.
Estimators inherit from BaseEstimator and mixins — which is why GridSearchCV can tune ANY model: it only speaks the inherited contract.
class DataSource:
def __init__(self, name):
self.name = name
def describe(self):
return f'source: {self.name}'
class CsvSource(DataSource):
pass # inherits EVERYTHING
src = CsvSource('sales.csv')
print(src.describe()) # parent method, child instance
print(isinstance(src, CsvSource), isinstance(src, DataSource))describe isn't defined on CsvSource, so lookup climbs: instance → CsvSource → DataSource. And isinstance says a CsvSource IS-A DataSource too — subclass instances pass both checks, which is what lets code written for the parent accept any child.
A parent class is the franchise operations manual: how to open, how to close, how to greet. Each location (subclass) follows the manual by default — but the airport branch overrides the opening-hours chapter, and the flagship store ADDS a chapter (new method) about events. When a chapter is overridden, staff read the LOCAL version first; for everything else, the corporate manual answers. super() is the local manager saying 'do what corporate says, THEN my extra steps'.
Define a method with the parent's name and child instances use YOURS — lookup finds it before climbing. The design rule that keeps this safe (the Liskov principle, informally): an override should honor the parent's contract — same kind of inputs, same kind of outputs, no new surprises — so any code that works with the parent still works with the child. Override to change HOW, never to change WHAT.
class Report:
def __init__(self, title, rows):
self.title = title
self.rows = rows
def render(self):
return f'{self.title}: {len(self.rows)} rows'
class AuditedReport(Report):
def __init__(self, title, rows, auditor):
super().__init__(title, rows) # parent sets up ITS state
self.auditor = auditor # child adds its own
def render(self):
return super().render() + f' [audited by {self.auditor}]'
r = AuditedReport('Q3 Sales', [1, 2, 3], 'kai')
print(r.render())The two super() patterns in one class. In __init__: let the parent initialize its attributes, then add yours — skip that call and self.title never exists (the #1 inheritance bug). In render: EXTEND rather than replace — run the parent's version, decorate its result.
Inherit only when the child IS a kind of the parent — a CsvSource IS-A DataSource; an AuditedReport IS-A Report. When one thing USES another, that's HAS-A: a Pipeline HAS sources (store them as attributes — composition), it isn't a kind of source. The classic mistake: inheriting to grab convenient methods (class Report(list) for free append) — now your report has sort(), pop(), and 30 other methods that make no sense for reports, and every one is a bug invitation. Composition: reports HAVE a list of rows.
One, occasionally two levels. Deep towers (A→B→C→D→E) make every behavior a scavenger hunt across five files, and Python's multiple inheritance (class C(A, B)) adds ordering rules (the MRO — method resolution order, left-to-right) that are easy to get wrong; treat it as read-only knowledge for understanding library mixins, not a tool to reach for. If you're inheriting just to share a utility function, a plain module-level function or composition is the honest design. Modern codebases inherit sparingly and compose liberally.
src.load() on a CsvSource: where Python searches, in order. Click each level.
A premium user is a user with one extra ability.
PremiumUser defines no __init__, so the parent's runs on construction. greet() resolves one level up; export_data() exists only on the child. Inheritance at its simplest: everything plus one.
hi, ada ada.zip ready
Your task
Build a small hierarchy: Shape has __init__(self, label) and describe() returning '<label>: area=<area()>'. Its area() raises NotImplementedError. Rectangle(label, w, h) and Circle(label, r) chain super().__init__ and override area() (circle: 3.14159 * r * r, rounded to 2). Loop a list of shapes and print each describe() — the parent method driving child area().
desk: area=0.72 table: area=0.79
Write your solution in the editor on the right, then hit Run.
class B(A): pass — what can instances of B do?
A subclass defines __init__ with extra parameters. What must it usually do first?
csv = CsvSource('sales') where CsvSource(DataSource). Which is True?
What's the design critique?
Base class Employee: __init__(name, salary); payslip() returns '<name>: $<salary>'. Subclass Manager adds a bonus parameter (chain super().__init__) and overrides payslip() to return the parent's result plus ' +$<bonus> bonus' using super(). Print payslips for Employee('ada', 90000) and Manager('kai', 110000, 15000). Expected: ada: $90000 kai: $110000 +$15000 bonus
Parent method run() calls self.process(). A child overrides process() only. What happens on child.run()?
Why, and what's the fix?
What does super() do, and why is super().__init__() so important in subclass constructors?
super() returns a proxy that delegates attribute lookup to the next class up the hierarchy (technically, the next in method resolution order), letting a child invoke the parent's version of a method it has overridden. Its two idioms: in an overridden method, extend rather than replace — return super().render() + suffix; and in __init__, chain construction. The chaining matters because defining __init__ in a child REPLACES the parent's — Python runs exactly one __init__ per construction — so parent attributes are simply never created unless the child explicitly calls super().__init__(...). The failure mode is deceptive: construction succeeds, and the crash arrives later as AttributeError inside some inherited method that reaches for self.log or self.name. Convention: call super().__init__ FIRST, so parent state exists before child code that might depend on it runs.
Inheritance vs composition — how do you choose, and what goes wrong when teams over-inherit?
The test is the relationship: IS-A → inheritance (a CsvSource is a DataSource; an AuditedReport is a Report), HAS-A/USES-A → composition (a Pipeline has sources; a Report has rows). Inheritance buys polymorphism — parent-typed code runs every child — but the price is total coupling: the child imports the parent's ENTIRE contract, and every parent change ripples down. Over-inheritance failure modes: inheriting for convenience (class Report(list) exposes sort/pop/reverse that violate the domain's rules), deep towers where finding any behavior means spelunking five files, and base classes that accrete flags to serve incompatible children. Composition keeps surfaces intentional — the wrapper exposes exactly the methods that make sense — at the cost of writing small delegating methods. My defaults: inherit for a family of interchangeable VARIANTS behind one contract (the sklearn shape), compose for everything else, keep hierarchies to one or two levels, and treat 'I just want its methods' as a composition signal, never an inheritance one.
Explain polymorphism and the template method pattern with a concrete example.
Polymorphism: code that speaks a base-class contract runs correctly for any subclass, dispatching each call to the instance's actual override — for n in notifiers: n.send(msg) formats per channel with no if-chains, and adding a channel means adding a class, not editing every dispatch site. The template method builds on one detail of Python's lookup: attribute resolution starts at the instance's REAL class even inside parent-defined code. So a parent defines the invariant skeleton — def run(self): validate; result = self.process(clean); report — and self.process resolves to whatever the child defined. The base's process raises NotImplementedError, making a forgotten override a loud, named failure instead of silent wrong behavior. This 'inversion of control' — the framework calls YOU — is the architecture of PyTorch (nn.Module.__call__ drives your forward), Airflow (BaseOperator drives your execute), and unittest (the runner drives your test methods): you write one method; inheritance wires it into machinery you never touch.
Common Mistakes to Avoid
1) Child __init__ without super().__init__ — parent attributes never exist; crash arrives later, elsewhere. 2) Inheriting for convenience (Report(list)) — IS-A fails, foreign methods leak; compose. 3) Overrides that change the contract — different parameter meanings or return types break every parent-typed caller. 4) Deep hierarchies — one or two levels; beyond that, refactor to composition. 5) Forgetting that parent code calling self.method() dispatches to the CHILD — both a superpower (templates) and a surprise (your override runs where you didn't expect). 6) type(x) == Parent checks — use isinstance, which honors subclasses.
Ask the AI Tutor
Try these prompts in the AI Tutor panel: • 'Quiz me: IS-A or HAS-A for these eight designs?' • 'Trace this method call up the hierarchy step by step.' • 'Show me the missing-super().__init__ bug and its delayed crash.' • 'Refactor this if-elif type-switch into polymorphic classes with me.' • 'Interview mode: ask me inheritance vs composition and grade my answer.'
Glossary
Inheritance — a class building on another, reusing its behavior. Parent/base class — the class inherited from. Child/subclass — the inheriting class (class B(A):). Override — redefining a parent method; child wins lookup. super() — proxy to the next class up; super().__init__ chains construction. IS-A / HAS-A — the inherit-vs-compose test. Composition — holding other objects as attributes instead of inheriting. Polymorphism — one call, per-subclass behavior. Template method — parent skeleton calling child-overridden steps. NotImplementedError — the base's 'you must override this' signal. MRO — method resolution order for multiple inheritance. Liskov principle — overrides must honor the parent's contract.
Recommended Resources
• Docs: 'Inheritance' in the official tutorial's classes chapter (9.5–9.6). • Read: sklearn's 'Developing scikit-learn estimators' — watch BaseEstimator + mixins hand you fit/predict machinery. • Practice: take the Notifier example and add a PagerDutyNotifier WITHOUT touching the loop — feel the open-for-extension payoff. • Next in DSM: children can override anything — so how do objects protect their internals? Encapsulation & Properties covers naming conventions, @property, and validated attributes.
Recap
✓ class Child(Parent) inherits everything; lookup climbs instance → child → parent → object. ✓ Override to change HOW (honor the contract); extend with super().method(). ✓ Child __init__ must chain super().__init__ — or parent state never exists. ✓ isinstance honors the tree: a child IS-A parent everywhere parent-typed code runs. ✓ Parent code calling self.step() dispatches to child overrides — the template pattern behind PyTorch/Airflow. ✓ Inherit for variant families (IS-A); compose for assemblies (HAS-A); stay shallow. Next up: Encapsulation & Properties. Inheritance shares internals freely — now learn to protect them: private-by-convention naming, @property for computed attributes, and setters that validate.
Run your code to see the output here.