Inheritance and composition
A subclass reuses a parent's behaviour and overrides the parts that differ, with super() calling the parent. Then see composition, where one object holds another, and why ADK agents are configured rather than subclassed.
Inheritance lets one class build on another. class SavingsAccount(BankAccount): says a savings account is a bank account: it gets every attribute and method of BankAccount for free, and can add new ones or override the ones that should behave differently. Inside an override, super() reaches the parent’s version, so you add a rule and reuse the rest instead of copying it.
Composition is the other way to reuse: one object holds another as an attribute. A UPI wallet is not a kind of bank account, but it has one, and it pays by asking that account to withdraw. The wallet works with any account that has a withdraw method, which keeps the two classes independent.
The usual advice is “prefer composition”. Subclassing is right when the child truly is a special case of the parent and you want code that accepts the parent to accept the child too. Composition is right when you just want to use another object’s abilities, and it stays easy to change later.
Press Run for the inheritance scenario, then switch Try it to “Composition” to see the wallet.
# Inheritance: a SavingsAccount is a BankAccount with one extra rule.
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def withdraw(self, amount):
if amount > self.balance:
return "Insufficient funds"
self.balance -= amount
return f"Withdrew {amount}, balance {self.balance}"
class SavingsAccount(BankAccount):
def __init__(self, owner, balance=0, min_balance=1000):
super().__init__(owner, balance) # let the parent set the basics
self.min_balance = min_balance
def withdraw(self, amount): # override: add a check, then reuse the parent
if self.balance - amount < self.min_balance:
return f"Keep at least {self.min_balance}"
return super().withdraw(amount)
# Composition: a wallet HAS an account. It is not a kind of account.
class UpiWallet:
def __init__(self, account):
self.account = account
def pay(self, amount):
return "UPI: " + self.account.withdraw(amount)
basic = BankAccount("Asha", 5000)
savings = SavingsAccount("Ravi", 5000)
print(basic.withdraw(4500))
print(savings.withdraw(4500))
print(savings.withdraw(2000))
print(isinstance(savings, BankAccount))
wallet = UpiWallet(savings)
print(wallet.pay(500))
print(wallet.account.balance)Press Play to run the program line by line, or step with ← →.
Variables
Nothing defined yet.
Output
Nothing printed yet.
What to notice
- Lookup starts at the child.
savings.withdraw(...)findswithdrawinSavingsAccountfirst, so the override runs. If the child had nowithdraw, Python would keep looking inBankAccount. Attribute lookup walks up the parents until it finds a match. super()is how an override reuses the parent.super().__init__(owner, balance)sets the shared attributes;super().withdraw(amount)does the actual withdrawal after the child’s extra check. The same object isselfthroughout.- The override can refuse without ever calling the parent. “Keep at least 1000” came back before any balance changed. An override decides when, and whether, to delegate.
isinstance(savings, BankAccount)isTrue. A child object counts as its parent type. That is the promise inheritance makes: anything that works with aBankAccountworks with aSavingsAccount.- Composition delegates instead of inheriting.
UpiWallet.paycallsself.account.withdraw. Swap the account for a plainBankAccountand nothing in the wallet changes. Changing a parent class, by contrast, changes every child.
Inherit when
The child is a special case of the parent (a savings account is an account), and callers should accept both. Overriding one or two methods is enough to describe the difference.
Compose when
You want to use another object’s abilities (a wallet uses an account). The relationship is has-a, the parts can be swapped, and you avoid a growing tree of subclasses.
Why this matters for AI work
ADK’s agents are classes you configure, not classes you subclass. You create Agent(name=..., model=..., instruction=..., tools=[...]) and customise behaviour through those arguments and through callbacks. The ADK 2.0 guide says overriding execution methods such as _run_async_impl is no longer the correct way to drive execution: the workflow engine bypasses those overrides, so logic placed there is silently ignored. The supported route is composition, in effect: pass tools, sub-agents and BeforeAgentCallback / AfterAgentCallback hooks into an agent rather than inheriting from it. Knowing both patterns lets you recognise when framework code is asking for one and not the other.