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.

10 min Free

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.

Ready
# 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 ← →.

00/11

Variables

Nothing defined yet.

Output

Nothing printed yet.

What to notice

  1. Lookup starts at the child. savings.withdraw(...) finds withdraw in SavingsAccount first, so the override runs. If the child had no withdraw, Python would keep looking in BankAccount. Attribute lookup walks up the parents until it finds a match.
  2. 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 is self throughout.
  3. 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.
  4. isinstance(savings, BankAccount) is True. A child object counts as its parent type. That is the promise inheritance makes: anything that works with a BankAccount works with a SavingsAccount.
  5. Composition delegates instead of inheriting. UpiWallet.pay calls self.account.withdraw. Swap the account for a plain BankAccount and 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.

Check your understanding

1. SavingsAccount defines its own withdraw. What does super().withdraw(amount) inside it do?
2. A wallet is not a kind of account but needs to withdraw money. Which design fits?
3. How does the ADK 2.0 guide say to customise an agent's behaviour?