Object-Oriented Design in Python
A class defines a type and its behavior. Calling the class creates an instance;
__init__ initializes that already-created instance. It is therefore more
precise to call __init__ an initializer rather than the constructor itself.
class Account:
institution = "Example Credit Union" # shared class attribute
def __init__(self, owner: str, balance: int = 0) -> None:
if balance < 0:
raise ValueError("opening balance cannot be negative")
self.owner = owner # instance attributes
self._balance = balance
def deposit(self, amount: int) -> None:
if amount <= 0:
raise ValueError("amount must be positive")
self._balance += amount
@property
def balance(self) -> int:
return self._balance
An instance method is a function stored on the class. Attribute lookup binds the
instance as the first argument, conventionally named self.
State and invariants
Keep instance state on the instance. A mutable class attribute is shared by all instances and is a common source of accidental global state.
Methods are useful when they preserve an invariant: an Account can reject an
invalid deposit instead of exposing arbitrary balance mutation. Properties can
add validation or computation while retaining attribute syntax, but plain public
attributes are appropriate when no invariant needs protection.
Python's leading underscore is a non-public convention, not an access-control boundary. Double-leading names trigger name mangling, which helps avoid subclass collisions but does not provide secrecy.
Choose the smallest model
- Use a plain function when behavior does not need persistent state.
- Use a
dataclasses.dataclassfor a record-like value with generated boilerplate. - Use composition when one object can delegate to another.
- Use inheritance when the subtype genuinely satisfies the parent's behavioral contract, not merely to reuse a few lines of code.
Python code often depends on behavior rather than a concrete inheritance tree.
An object that supports the required protocol can be accepted regardless of its
class. Static type checkers can express this with typing.Protocol.
@classmethod receives the class and is useful for alternate constructors.
@staticmethod receives neither class nor instance; if it is not meaningfully
part of the type's namespace, a module-level function is usually simpler.
Special methods such as __repr__, __eq__, and __len__ integrate a type with
Python's data model. Implement them according to their documented contracts; do
not call special methods directly when the corresponding built-in operation
exists.