Object-oriented programming in Python works differently from the languages it borrowed the vocabulary
from. There are no interfaces to declare and no private to enforce. What matters is what an
object can do, and this post covers how that changes the advice.
Inheritance
class Account:
def __init__(self, number, balance):
self.number = number
self.balance = balance
def describe(self):
return f"{self.kind()} {self.number}: ${self.balance:,.2f}"
def kind(self):
return "Account"
class Savings(Account):
def __init__(self, number, balance, rate):
super().__init__(number, balance)
self.rate = rate
def kind(self):
return "Savings"
def add_interest(self):
self.balance += self.balance * self.rate
s = Savings("2001-0001", 8400.50, 0.02)
s.add_interest()
print(s.describe()) # Output: Savings 2001-0001: $8,568.51
class Savings(Account) inherits everything. super().__init__(...) runs the
parent's initialiser — call it first, before your own setup, and do not skip it or the parent's
attributes never get created.
Notice what describe() did: it is defined on Account, but it called
kind() and got the subclass version. That is polymorphism, and Python does it by
default — every method is overridable and lookup always starts at the actual class.
Duck typing: no interface required
Python does not care what class an object is, only whether it has the method you are about to call:
class Card:
def pay(self, amount):
return f"charged ${amount} to card"
class BankTransfer:
def pay(self, amount):
return f"transferred ${amount}"
def checkout(method, amount):
return method.pay(amount) # no shared base class needed
print(checkout(Card(), 25)) # Output: charged $25 to card
print(checkout(BankTransfer(), 25)) # Output: transferred $25
These two classes are unrelated. Neither declares that it implements anything. checkout
works because both have a pay method — "if it walks like a duck".
The consequence for design: an interface in Python is a set of method names people agree on, not a declaration. That is why you will see far less inheritance in Python than in Java — most of what inheritance is used for there is achieved here by simply having the right methods.
Abstract base classes, when you want the promise enforced
Duck typing fails late — you find out a method is missing when something calls it. When that is not
good enough, abc makes the requirement explicit:
from abc import ABC, abstractmethod
class PaymentMethod(ABC):
@abstractmethod
def pay(self, amount): ...
class Voucher(PaymentMethod):
pass # never implemented pay()
Voucher()
That raises TypeError: Can't instantiate abstract class Voucher at the moment you try to
create one, rather than later when something calls pay. The class becomes a contract you
cannot half-implement.
Use one when several classes must be interchangeable and forgetting a method is a real risk. For two classes in the same file, duck typing is enough.
Properties instead of getters and setters
Do not write get_balance() and set_balance(). Expose the attribute, and if
you later need logic, add it without changing a single caller:
class Account:
def __init__(self, balance):
self._balance = balance
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, value):
if value < 0:
raise ValueError("balance cannot be negative")
self._balance = value
a = Account(100)
a.balance = 250
print(a.balance) # Output: 250
try:
a.balance = -1
except ValueError as error:
print(error) # Output: balance cannot be negative
Callers still write a.balance and a.balance = 250. This is why Java's habit
of writing getters for every field up front is unnecessary here: you can start with a plain attribute
and introduce a property later without breaking anything.
Prefer composition
Inheritance says "is a"; composition says "has a". The second is right more often:
class InterestCalculator:
def __init__(self, rate):
self.rate = rate
def yearly(self, balance):
return round(balance * self.rate, 2)
class Account:
def __init__(self, balance, calculator):
self.balance = balance
self.calculator = calculator # HAS a calculator
def projected(self):
return self.balance + self.calculator.yearly(self.balance)
a = Account(1000.0, InterestCalculator(0.05))
print(a.projected()) # Output: 1050.0
The calculator can be swapped, tested on its own, and reused by something that is not an account.
A class SavingsAccount(Account) hierarchy that grows a branch per product ends up with
subclasses inheriting behaviour they do not want.
The test: if you are inheriting to reuse code rather than because the subclass genuinely is a kind of the parent, use composition.
Exceptions are the clearest hierarchy you will write
Where inheritance does earn its place is exception types. The bank app this track draws on defines
one base class so a single except catches everything the app raises on purpose:
class BankError(Exception):
...
class AuthenticationError(BankError):
...
class ValidationError(BankError):
...
class InsufficientFundsError(BankError):
...
(The docstrings are elided here; in the app each class carries one explaining exactly when it is raised, which is the only documentation an exception class usually needs.)
except BankError then catches every rule the app enforces and nothing else — while
except Exception would also swallow the typos in your own code.
Exception Handling covers this properly.
The method resolution order
When a class has more than one parent, Python needs a rule for which method wins. That rule has a name and you can read it:
class Auditable:
def save(self):
return "audited"
class Timestamped:
def save(self):
return "timestamped"
class Account(Auditable, Timestamped):
pass
print(Account().save()) # Output: audited
print([c.__name__ for c in Account.__mro__])
# Output: ['Account', 'Auditable', 'Timestamped', 'object']
__mro__ is the method resolution order — the exact list Python walks, left to right,
looking for a name. Auditable comes first in the class statement, so its
save wins.
Every class ends at object, which is where the default __repr__ and
__eq__ you have been overriding come from. When multiple inheritance confuses you, print
the MRO — it turns a guess into a fact.
What Python does not have
- No
private. A leading underscore is a convention. There is no keyword and no enforcement. - No method overloading. One method per name — a second
defreplaces the first. Use default arguments or*argsinstead. - No interfaces. Duck typing, or an ABC when you want it checked.
- Multiple inheritance is allowed, and mostly used for small mixins rather than two real parents.
Next: Dataclasses & Type Hints, which removes most of the boilerplate from everything above.