The best Python OOP code gives every object a clear responsibility, keeps ownership of state explicit, and chooses the simplest relationship that expresses the design. Use a class when data and behavior belong together; use a dataclass when a type is mainly named data; use delegation when one object uses another service; and reserve inheritance for a genuine substitutable relationship.
These choices matter more than how many classes a program contains. The sections below show how to make them deliberately, with concrete Python patterns and the failure modes that make object-oriented code difficult to maintain.
As an Amazon Associate I earn from qualifying purchases.
Start with responsibility, not with a class hierarchy
The Python tutorial describes the purpose of a class succinctly: “Classes provide a means of bundling data and functionality together.” That does not mean every function or data structure needs a class. A class earns its place when an object owns state, enforces an invariant, provides behavior over that state, or represents a concept that appears repeatedly in the program.
Free tools Windows power users keep installed
One-click scans. No signup required.
Before writing class, answer three questions:
- What state does this object own, and is that state unique to one instance?
- What operations are naturally responsible for changing or inspecting that state?
- What relationship does this object have with other objects: specialization, use, or merely a shared interface?
If the answers are unclear, a function, a module-level constant, or a simple collection may be clearer. Object-oriented design is a tool, not a requirement for every Python problem.
#1 Best Overall
Make state ownership explicit
Python gives you both instance attributes and class attributes. An instance attribute belongs to one object; a class attribute is looked up on the class and is shared by instances unless an instance assignment shadows it. That distinction should be visible in the class design.
Put per-instance data in the initializer
Values that can differ between objects belong on self, normally initialized in __init__. This includes mutable containers such as lists, dictionaries, and sets.
class ShoppingCart:
def __init__(self, owner):
self.owner = owner
self.items = []
def add(self, item):
self.items.append(item)
Each ShoppingCart now receives its own list. A caller can create two carts without one cart’s additions appearing in the other.
Use class attributes only for intentional shared values
A class attribute is appropriate for a constant or a value deliberately shared by all instances, such as a default tax rate that is changed centrally. It is not an appropriate default location for per-object mutable state.
The Python tutorial illustrates the trap with a Dog.tricks list: two dogs append to the same list when tricks is declared on the class. Moving tricks = [] into __init__ gives each dog a separate collection. See the tutorial’s class examples at docs.python.org/3/tutorial/classes.html.
class Dog:
tricks = [] # shared by every dog: usually a bug
def __init__(self, name):
self.name = name
# Safer:
class Dog:
def __init__(self, name):
self.name = name
self.tricks = []
def learn(self, trick):
self.tricks.append(trick)
Decide whether state belongs outside the object
Some values are request-scoped, temporary, or derived from a database and do not belong as long-lived attributes. Keeping them in a function or a separate service can prevent objects from becoming implicit global state. A useful test is ownership: if an object cannot explain why it stores a value or how it keeps that value valid, the value probably belongs elsewhere.
Put behavior beside the state it protects
Methods are most useful when they express operations on the object’s own data and preserve its invariants. Instead of exposing several fields and asking every caller to update them in the right order, provide one operation that performs the change safely.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Clear out junk files and repair common Windows errors3Scan for outdated or missing drivers - takes under a minuteRank #2
class BankAccount:
def __init__(self, balance=0):
if balance < 0:
raise ValueError("balance cannot be negative")
self._balance = balance
@property
def balance(self):
return self._balance
def withdraw(self, amount):
if amount <= 0:
raise ValueError("amount must be positive")
if amount > self._balance:
raise ValueError("insufficient funds")
self._balance -= amount
The caller does not need to know how the balance is stored or which checks precede a withdrawal. This is encapsulation in a practical sense: the object owns the rule and the transition.
Keep the public surface intentional
Give public methods and attributes names that describe stable operations. Prefix implementation details with a single underscore when they are not part of the intended external interface, and avoid exposing mutable internals when callers could accidentally violate invariants. Return a copy or an immutable view when that is necessary to keep ownership clear.
Do not create a method solely to mirror an attribute. A property is useful when reading a value requires computation, validation, or a compatibility layer; otherwise a plain attribute is often easier to understand.
Choose a dataclass for data-shaped types
When a type mainly bundles a few named data items, a dataclass is Python’s idiomatic option. It removes repetitive constructor and representation code while keeping the fields explicit. The tutorial identifies dataclasses as the natural approach for a record-like structure similar to a Pascal record or C struct.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
Use a regular class instead when the type has substantial behavior, carefully controlled construction, a complex invariant, or an API that should not expose all of its fields as record-like data.
| Design need | Usually the clearest choice | Reason |
|---|---|---|
| Named values with little behavior | Dataclass | Communicates the data shape and avoids ceremony. |
| State transitions and invariants | Regular class with methods | Operations can validate and control changes. |
| A service used by another object | Delegation | The using object stays focused while the service owns its work. |
| A true specialized form of an existing type | Inheritance | Overrides and shared behavior express a substitutable relationship. |
| Interchangeable behavior without a nominal hierarchy | Duck typing or an ABC | The contract can stay informal or become explicit as the design requires. |
Prefer delegation when an object uses another object
Delegation is an object-oriented technique in which one object forwards a responsibility to another object that is better suited to perform it. The Python Programming FAQ calls delegation “an object-oriented technique (also called a design pattern).”
class EmailSender:
def send(self, address, message):
# transport details live here
print(f"Sending to {address}: {message}")
class Registration:
def __init__(self, sender):
self.sender = sender
def confirm(self, user):
self.sender.send(user.email, "Your registration is complete")
Registration owns registration policy; EmailSender owns delivery. The dependency can be replaced with a test double or another sender without making Registration inherit from a transport class.
Do not replace every type check mechanically
Repeated branches such as “if this is type A, do one thing; if it is type B, do another” can signal that behavior belongs on the objects themselves or in a delegated service. Moving the operation to the relevant object often makes extension easier. This is guidance, not an absolute ban: a type check is reasonable at a boundary where the program must reject unsupported input, select a serialization format, or handle genuinely different external protocols.
Use inheritance only for a meaningful specialization
Inheritance is appropriate when the derived object is a specialized form of the base object and callers can use it wherever the base is expected. It is not a good reason to inherit merely to reuse a few lines of implementation.
class PaymentMethod:
def authorize(self, amount):
raise NotImplementedError
class CardPayment(PaymentMethod):
def authorize(self, amount):
return self._contact_gateway(amount)
def _contact_gateway(self, amount):
return True
Here, a card payment is a payment method and supplies the same authorization operation. If the relationship is instead “a checkout process uses a gateway,” composition and delegation describe it more accurately.
Override deliberately
An override should preserve the base method’s expected contract: accepted inputs, returned results, and important exceptions. If a subclass must disable most of the base behavior, the hierarchy may be expressing the wrong relationship. Keep overridden methods small and document any changed guarantees.
Understand method resolution order
Python supports multiple inheritance. In a diamond-shaped hierarchy, method lookup follows a defined method resolution order (MRO), and cooperative calls to super() allow each class in that order to participate. The order is dynamic and designed to handle diamonds, so the behavior is predictable but not trivial.
class Logged:
def save(self):
print("log")
super().save()
class Stored:
def save(self):
print("store")
class LoggedStored(Logged, Stored):
pass
LoggedStored().save() # log, then store
Use multiple inheritance when the combination is a deliberate part of the design, especially for small mixins with cooperative methods. Every participating method should call super() with compatible arguments. If readers must trace a tangled graph to know which implementation runs, delegation is usually easier to maintain.
Choose duck typing or an ABC according to the contract
Python supports duck typing: a consumer can accept any object that supplies the operations it needs, without requiring inheritance from a particular class. This keeps APIs flexible and is often enough for a small internal boundary.
def write_report(writer, text):
writer.write(text)
write_report needs a write method; a file-like object, an in-memory buffer, or a test double can provide it. The consumer depends on behavior rather than a nominal type.
Make an explicit contract with abc
An abstract base class (ABC) is useful when the contract should be documented and checked as part of the type design, or when several implementations share behavior.
Recommended Free Tools
from abc import ABC, abstractmethod
class Cache(ABC):
@abstractmethod
def get(self, key):
"""Return a value or None when the key is absent."""
@abstractmethod
def put(self, key, value):
"""Store a value under key."""
The abc documentation provides the infrastructure for ABCs. Use it when an explicit nominal contract adds value, not as a ceremonial base class for every protocol.
Know what collections.abc recognizes
The collections.abc documentation describes several ways an object can satisfy an interface: direct inheritance, virtual subclass registration, and recognition of some simple method-based interfaces. Complex semantics cannot always be inferred merely from method names. If an interface requires ordering guarantees, side effects, or coordinated methods, document and test those rules explicitly.
Keep equality and hashing consistent
Decide what it means for two instances to be equal before implementing __eq__. If equality represents value identity, compare the fields that define that value; if it represents object identity, the default behavior may already be correct.
class UserId:
def __init__(self, value):
self.value = value
def __eq__(self, other):
if not isinstance(other, UserId):
return NotImplemented
return self.value == other.value
Hashing must match equality. The language reference warns that when a class defines __eq__() but not __hash__(), its instances cannot be used in hashable collections such as sets or dictionary keys. Read the complete rule in the Python data model reference.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Choose a safe value policy
- If instances are mutable and equality can change over time, leave them unhashable rather than allowing a key to move between hash buckets.
- If instances are immutable value objects, implement equality and a matching hash over the same fields.
- If identity is the meaning, do not add value-based equality just to make tests convenient.
Test the contract directly: equal objects should compare equal, and any objects used as dictionary keys should remain stable for as long as they are stored.
Best Value
A practical design workflow
- Name the responsibility. Write one sentence describing what the object owns and does. If the sentence contains several unrelated verbs, split the design.
- List the state. Mark each value as per-instance, intentionally shared, derived, temporary, or external. Initialize per-instance mutable values in
__init__. - Protect invariants. Put validation and state transitions in methods on the object that owns the rule.
- Classify relationships. Use inheritance for a meaningful specialization; use delegation when one object consumes another object’s service.
- Set the contract. Start with duck typing when only a few operations are needed. Introduce an ABC when a documented nominal contract or shared implementation is valuable.
- Select the data representation. Use a dataclass for a primarily record-like type; use a regular class when behavior and invariants dominate.
- Review equality. Decide whether instances are identity objects or value objects, then align
__eq__and__hash__. - Trace extension. For multiple inheritance, inspect the MRO and verify cooperative
super()calls. If the path is difficult to explain, simplify it.
Common failure modes and their fixes
Every instance unexpectedly shares a list
Symptom: adding an item to one object changes another object. Cause: a mutable list or dictionary was defined as a class attribute. Fix: create it in __init__, as in the Dog example above.
A subclass is full of exceptions to the base class
Symptom: overridden methods reject inputs the base accepted, return unrelated types, or disable core behavior. Cause: inheritance was chosen for code reuse rather than specialization. Fix: extract a smaller shared contract, use delegation, or make the classes independent.
Callers contain long type-switch chains
Symptom: every new implementation requires editing several isinstance branches. Cause: behavior is centralized in the consumer instead of supplied by the objects or a delegated strategy. Fix: move the operation behind a common method when the semantics are genuinely shared. Retain explicit checks where they are part of input validation or protocol handling.
An object cannot be used in a set after adding equality
Symptom: inserting an instance into a set raises a “unhashable type” error. Cause: __eq__ was defined without a compatible __hash__. Fix: keep the object unhashable if it is mutable, or implement a stable hash over the same immutable value fields used for equality.
Multiple inheritance calls methods twice or not at all
Symptom: initialization or another operation is skipped or duplicated. Cause: classes in the MRO do not cooperate consistently with super(), or signatures are incompatible. Fix: inspect Class.__mro__, make cooperative methods call super() exactly once, and keep their signatures compatible. If that discipline is not worth the complexity, delegate instead.
Putting the choices together
The following small design separates data, policy, and an external service without forcing them into one hierarchy.
from dataclasses import dataclass
@dataclass(frozen=True)
class Money:
amount: int
currency: str
class PaymentGateway:
def charge(self, money):
raise NotImplementedError
class Checkout:
def __init__(self, gateway):
self.gateway = gateway
self._total = Money(0, "USD")
@property
def total(self):
return self._total
def add(self, price):
if price.currency != self._total.currency:
raise ValueError("currency mismatch")
self._total = Money(
self._total.amount + price.amount,
self._total.currency,
)
def pay(self):
return self.gateway.charge(self._total)
Money is a value-shaped type, so a dataclass communicates its fields and value semantics. Checkout owns the total and its currency invariant. The gateway is delegated the external charge operation. A concrete gateway can supply the required charge behavior without inheriting from Checkout; an ABC could be introduced later if the gateway contract needs to be formalized for many implementations.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Quick Recap
Final review checklist
- Can you state each class’s responsibility in one sentence?
- Does every mutable value belong to the object that owns it?
- Are class attributes intentionally shared rather than accidental shared state?
- Do methods enforce the invariants of their own data?
- Is inheritance expressing a real specialization rather than convenient reuse?
- Would delegation make a service relationship clearer?
- Is duck typing sufficient, or does an ABC provide a useful explicit contract?
- Is a dataclass clearer because the type is primarily named data?
- Do equality and hashing describe the same notion of identity?
- Can another developer explain the MRO and cooperative
super()calls without guesswork?
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

