Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

Elegant object-oriented Python is not about turning every function into a class. It is about giving each piece of state a clear owner, placing behavior beside the state it governs, and choosing the simplest relationship between types. Use a class when an object has identity, lifecycle, invariants, or behavior worth grouping; use a function or a plain data structure when that is clearer.

Python’s tutorial describes classes as a way of “bundling data and functionality together.” The practices below turn that idea into concrete design decisions: instance state instead of accidental shared state, dataclasses for record-like values, inheritance only for genuine substitutability, duck typing or abstract base classes for contracts, delegation for services, and deliberately matched equality and hashing.

Start with responsibility, not with a class hierarchy

Before writing class, describe the responsibility in one sentence: “A ShoppingCart calculates totals,” “a ReportExporter writes a report,” or “a Temperature represents a value with a unit.” If the sentence needs several unrelated verbs, the proposed class probably has too many responsibilities.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Ask three questions:

  • What state does this object own? Values that differ per object belong on the instance. Truly universal constants can be class attributes, and process-wide services may belong outside the object entirely.
  • What behavior must protect or interpret that state? Put operations that enforce an invariant near the data they change. A cart should add and remove lines rather than exposing callers to arbitrary list manipulation.
  • What relationship does another type have to this one? A specialized subtype may inherit; a collaborator that merely provides a service should usually be composed or delegated to.

These questions prevent “class for every noun” design. A parsing helper that has no durable state may be a function. A configuration record may be a dataclass. A long-running connection with open, send, retry, and close behavior is a stronger class candidate.

#1 Best Overall
Elebase USB to USB C Adapter for iPhone 18 Pro Max,USBC Car Charger Adapter | 4Pack Type C Female to A Male Cable Converter for Apple Duo 17 16 15 14 Plus,iWatch Watch 12 11 10,iPad 9 8 Air,Airpods 5 4
  • Read Before You Buy — No Video Output: These adapters support charging and USB 2.0 data transfer, but cannot transmit video signals. Except for standard USB webcams (which use USB data only), they are not compatible with HDMI/DisplayPort cables, video-capable USB-C hubs, or docking stations with video output.
  • Convert USB-A Ports to USB-C: Designed to connect USB-C earphones, cables, flash drives, card readers, and other USB-C accessories to standard USB-A ports. Plug-and-play with no drivers or software required.
  • Aluminum Alloy Housing: Built with a sturdy aluminum alloy shell that aids in heat dissipation and protects against daily wear and scratches. Designed to maintain a stable and secure connection.
  • Compact & Travel-Friendly: The ultra-compact design allows the adapter to stay plugged into your device without blocking adjacent ports or adding bulk, reducing wear and tear on your original USB ports.
  • 12-Month Warranty: Backed by a 12-month manufacturer warranty for peace of mind. Designed to meet strict quality control standards for reliable everyday performance.

A practical state-ownership map

State kind Typical location Reason
Unique to one object Instance attribute initialized in __init__ or a dataclass field Each object gets an independent value and lifecycle
Constant for every instance Class attribute, preferably immutable One shared definition avoids duplication
Shared mutable resource Explicit service, manager, or injected collaborator Sharing is visible and can be synchronized or replaced
Temporary calculation Local variable or function return value It does not need object lifetime or identity

Writing this map before coding exposes hidden coupling. If several instances must coordinate through a cache, queue, or registry, make that shared object explicit rather than hiding it in an incidental class attribute.

Keep mutable state on each instance

The most common Python class bug is an accidental shared mutable attribute. A list, dictionary, set, or custom mutable object assigned in the class body is read by every instance unless an instance later shadows it.

class Dog:
    tricks = []                 # one list shared by every Dog

    def add_trick(self, trick):
        self.tricks.append(trick)

Two dogs now append to the same list. The Python tutorial demonstrates this exact failure and fixes it by creating the list in __init__: Python Tutorial, “9. Classes”.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
class Dog:
    def __init__(self):
        self.tricks = []         # a new list for this instance

    def add_trick(self, trick):
        self.tricks.append(trick)

Immutable class attributes are generally safe for shared defaults:

class RetryPolicy:
    max_attempts = 3
    backoff_seconds = (1, 2, 4)

If a default must be created per instance, use an initializer or a dataclass factory. Never use a mutable object as a default argument either; def __init__(self, tags=[]) has the same lifetime problem because the list is created once when the function is defined.

Make sharing intentional

Sometimes sharing is the requirement: a connection pool, metrics collector, or memoization store may be common to many objects. Represent it explicitly, document its lifetime, and inject it where practical:

class ImageService:
    def __init__(self, cache):
        self._cache = cache

    def fetch(self, image_id):
        if image_id not in self._cache:
            self._cache[image_id] = self._load(image_id)
        return self._cache[image_id]

Now tests can provide a fresh dictionary, and production code can provide a bounded or persistent cache. The ownership is visible instead of being an undocumented side effect of class lookup.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Rank #2
Anker USB-C Hub, 5-in-1 USB Hub for Laptops, 4K HDMI Multiport Adapter | USB C Dongle with 90W Max Power Delivery, 3×5Gbps USB A Data Ports, for MacBook, Dell, and More (Charger Not Included)
  • 5-in-1 USB-C Hub: Experience comprehensive connectivity featuring a Power Delivery input, two USB-A 2.0 ports, a USB-A 3.0 port, and an HDMI port. (Note: The USB-C power delivery input port is only for connecting an external wall charger to power your laptop and cannot power peripheral devices.)
  • 90W Pass-Through Charging: Achieve optimal charging with 90W pass-through power to your laptop, supported by a total input of 100W, with the hub reserving 10W for operational efficiency. (Note: Wall charger not included.)
  • Quick Data Transfers: Accelerate your productivity with rapid data transfers using a high-speed 5Gbps USB 3.0 port and two 480Mbps USB 2.0 ports.
  • 4K HDMI Display: Enhance your visual experience with a hub capable of delivering 4K resolution at 30Hz in both mirror and extend modes. Please note that this hub is compatible with MacBook (macOS 12 and newer), Windows 10 and 11, ChromeOS, and laptops equipped with DP Alt Mode and Power Delivery. Note: This device is not compatible with Linux.
  • What You Get: Anker USB-C Hub (5-in-1, 4K HDMI), welcome guide, 18-month warranty, and our friendly customer service.

Use dataclasses for data-shaped objects

When a type mainly bundles a few named values, a dataclass is usually more expressive than hand-written initialization, representation, and comparison methods. The Python tutorial identifies dataclasses as the idiomatic approach for record-like structures: Python Tutorial, “9. Classes”.

from dataclasses import dataclass, field

@dataclass
class User:
    user_id: int
    email: str
    roles: list[str] = field(default_factory=list)

default_factory creates a separate list for each User. A dataclass also makes the visible data shape easy to inspect and gives a useful representation for logs and debugging.

When a dataclass is the wrong abstraction

Do not use a dataclass merely because it is concise. If callers must mutate fields freely and the object cannot protect its invariants, a small set of functions may be clearer. Conversely, if every change must pass validation, expose behavior such as rename(), grant_role(), or change_limit() and keep the underlying fields private by convention.

Validation can live in __post_init__ for construction-time checks:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
from dataclasses import dataclass

@dataclass(frozen=True)
class Port:
    number: int

    def __post_init__(self):
        if not 1 <= self.number <= 65535:
            raise ValueError('port must be between 1 and 65535')

A frozen value object communicates that its value should not change after creation. If your object contains mutable members, freezing the attribute binding does not make those members deeply immutable; choose immutable member types when value semantics require it.

Choose inheritance only for a meaningful subtype

Inheritance is appropriate when the derived object is a substitutable form of the base object and the inherited behavior remains valid. A CsvExporter and a JsonExporter may both satisfy an exporter contract. A Report should not inherit from DatabaseConnection simply because it uses one method from that class.

Python supports overriding and multiple inheritance. Method lookup follows a dynamic method resolution order (MRO) designed to handle diamond-shaped hierarchies and cooperative calls to super(). The mechanics are documented in the classes tutorial: Python Tutorial, “9. Classes”.

Rank #3
Sale
Anker USB C Hub, 7in1 Multi-Port USB Adapter, 4K@60Hz USBC to HDMI Splitter | 85W Max PD, 2 USB 3.0 & 1 USBC Data Ports, SD/TF Card Reader, for Laptops/Mac and Other Type C Devices (Charger Not Included)
  • Sleek 7-in-1 USB-C Hub: Features an HDMI port, two USB-A 3.0 ports, and a USB-C data port, each providing 5Gbps transfer speeds. It also includes a USB-C PD input port for charging up to 100W and dual SD and TF card slots, all in a compact design.
  • Flawless 4K@60Hz Video with HDMI: Delivers exceptional clarity and smoothness with its 4K@60Hz HDMI port, making it ideal for high-definition presentations and entertainment. (Note: Only the HDMI port supports video projection; the USB-C port is for data transfer only.)
  • Double Up on Efficiency: The two USB-A 3.0 ports and a USB-C port support a fast 5Gbps data rate, significantly boosting your transfer speeds and improving productivity.
  • Fast and Reliable 85W Charging: Offers high-capacity, speedy charging for laptops up to 85W, so you spend less time tethered to an outlet and more time being productive.
  • What You Get: Anker USB-C Hub (7-in-1), welcome guide, 18-month warranty, and our friendly customer service.

Override a stable contract

When overriding, preserve the base method’s expectations: accepted inputs, meaningful return values, and important exceptions. A subtype that silently changes “returns an empty result” into “raises for an empty result” can break callers even if the method name is unchanged.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
class Exporter:
    def export(self, rows):
        raise NotImplementedError

class JsonExporter(Exporter):
    def export(self, rows):
        return serialize_as_json(rows)

Keep the base contract small. A base class with many optional hooks forces subclasses to implement methods they do not need and makes substitution harder to reason about.

Understand cooperative multiple inheritance

Multiple inheritance can be useful for focused mixins such as logging, serialization, or access control. It becomes difficult when parent classes have incompatible initialization requirements or when a mixin silently depends on another parent.

class Logged:
    def save(self, record, **kwargs):
        print('saving', record)
        return super().save(record, **kwargs)

class Persisted:
    def save(self, record, **kwargs):
        return write_to_database(record)

class LoggedStore(Logged, Persisted):
    pass

For cooperative code, every participating method should accept and forward compatible arguments with super(). Inspect the actual order with LoggedStore.__mro__ when debugging. If a reader must mentally simulate several parents to know which method runs, delegation may be clearer.

Use duck typing when behavior is the real contract

Python code often does not need an object to inherit from a particular class. If a consumer only needs write() and flush(), any object supplying those operations can work. This is duck typing: depend on the behavior you call, not on a nominal parent class.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
def save_report(stream, text):
    stream.write(text)
    stream.flush()

A file, an in-memory buffer, or a test double can satisfy this small behavioral expectation without sharing an inheritance tree.

When an abstract base class helps

Use an abstract base class (ABC) when the contract itself is part of the design: you want named abstract methods, shared implementation, or a clear extension point. The abc module supplies the infrastructure for defining ABCs: Python documentation, abc.

Rank #4
Sale
UGREEN USB to USB C Adapter Combo 4-Pack, 10Gbps USB C Converter Space Gray | Data transfer & Fast charging for MacBook, iPhone 17/16/15, iPad, Samsung Galaxy, Apple Watch
  • Dual Converters, Infinite Potential:Includes 2× USB C male to USB A female adapters and 2× USB A male to USB C female adapters. Perfect for a wide range of uses—tablets with Bluetooth keyboards, expand USB ports on macbook, and more. Two different converters for all your daily needs
  • Next-Level 10Gbps & 3A Charging: No more slow 480Mbps, this usb to usb c adapter has a transfer speed of up to 10Gbps, allowing you to do more transferring in less time. This usb adapter fits both USB A and USB C charger, supporting up to 3A fast charging
  • Upgraded Exquisite Craftsmanship: With an aluminum alloy housing and metal connector, the usbc to usb adapter is extremely durable and sturdy. Rigorously tested to withstand more than 10,000 times of plugging and unplugging, ensuring long-lasting performance
  • Broad Compatible: The usb c to usb adapter widely supports all USB C/ USB A devices like laptops, tablets, cellphones, car chargers, and phone chargers. Such as compatible with MacBook Pro/Air 2023/2022, Thunderbolt 4/3 Devices,Apple MagSafe Watch 9/8/7/SE/Ultra, iPad Pro 2022/2021, Samsung Galaxy S23/S20/S10, and iPhone 17/16/15 Pro. Plug and play
  • Please Note: To reach 10Gbps speed, keep the cable under 3.3 ft. For USB A Male to USB C adapters, try flipping the USB C connector. USB C Male to USB A adapters support bidirectional 10Gbps transfer within 3.3 ft
from abc import ABC, abstractmethod

class Renderer(ABC):
    @abstractmethod
    def render(self, document):
        """Return the rendered representation."""

An ABC can also recognize virtual subclasses, and collections.abc documents direct inheritance, virtual registration, and recognition of some simple method-based interfaces: Python documentation, collections.abc. Do not assume that a few matching method names prove a complex semantic contract. A class may have a method called read without supporting the buffering, blocking, or error behavior your consumer requires.

Decision table: duck typing or ABC?

Need Prefer Why
A small internal consumer and a simple behavior Duck typing Fewer dependencies and easy test doubles
A public extension point with required methods ABC The contract is discoverable and incomplete implementations fail early
Shared default behavior across implementations ABC or a focused mixin Common code has one maintained home
Complex semantics not expressible by method names alone Explicit documentation, tests, and possibly an ABC Structural recognition alone cannot prove meaning

Prefer delegation for “uses” relationships

Delegation is an object-oriented technique in which one object forwards a responsibility to another. The Python FAQ calls it “an object-oriented technique (also called a design pattern)” and discusses it as a normal Python approach: Python Programming FAQ.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
class Checkout:
    def __init__(self, payment_gateway, inventory):
        self._payment_gateway = payment_gateway
        self._inventory = inventory

    def pay(self, order):
        self._inventory.reserve(order.items)
        return self._payment_gateway.charge(order.total, order.currency)

Checkout owns the checkout workflow; it does not pretend to be a payment gateway or inventory system. Each collaborator can evolve independently and can be replaced with a test double.

The FAQ favors methods that encapsulate behavior over repeatedly checking an object’s class and branching. That is a strong default, not an absolute ban on type checks. Type checks are legitimate at boundaries such as deserialization, plugin loading, or a deliberately closed set of formats. Keep the check in one place and convert it into a stable internal interface so the rest of the code does not become a chain of isinstance branches.

Protect invariants with small, intention-revealing methods

Public attributes are convenient until callers can create impossible states. If an account balance cannot be negative, do not make every caller remember that rule:

class Account:
    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 method names express valid operations, while the private-by-convention _balance keeps direct mutation out of normal use. Properties are useful for read-only views or calculated values; avoid writing a property for every field when direct dataclass fields are already clear.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Keep methods cohesive and predictable

  • Accept the narrowest useful inputs and return a stable, documented result.
  • Do not make a method secretly perform unrelated network, file, and user-interface work.
  • Keep side effects visible in names such as save, send, or delete; make queries such as total safe to call repeatedly.
  • Raise specific exceptions for violated preconditions, and preserve useful context when translating lower-level failures.
  • Use dependency injection for clocks, gateways, and storage when deterministic tests matter.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Treat equality and hashing as one design decision

Decide whether an object has identity semantics or value semantics. Two database sessions are usually different objects even if their settings match. Two coordinate values with the same components may represent the same value.

Best Value
Anker USB C Hub, 5-in-1 USBC to HDMI Splitter with 4K Display | 85W Max PD, 1 Powered USB-C 5Gbps & 2 Powered USB-A 3.0 5Gbps Data Ports, for MacBook Pro, MacBook Air, Dell and More (Black)
  • 5-in-1 Connectivity: Equipped with a 4K HDMI port, a 5 Gbps USB-C data port, two 5 Gbps USB-A ports, and a USB C 100W PD-IN port. Note: The USB C 100W PD-IN port supports only charging and does not support data transfer devices such as headphones or speakers.
  • Powerful Pass-Through Charging: Supports up to 85W pass-through charging so you can power up your laptop while you use the hub. Note: Pass-through charging requires a charger (not included). Note: To achieve full power for iPad, we recommend using a 45W wall charger.
  • Transfer Files in Seconds: Move files to and from your laptop at speeds of up to 5 Gbps via the USB-C and USB-A data ports. Note: The USB C 5Gbps Data port does not support video output.
  • HD Display: Connect to the HDMI port to stream or mirror content to an external monitor in resolutions of up to 4K@30Hz. Note: The USB-C ports do not support video output.
  • What You Get: Anker 332 USB-C Hub (5-in-1), welcome guide, our worry-free 18-month warranty, and friendly customer service.

If you implement __eq__(), you must consider __hash__() at the same time. The language reference warns that a class defining __eq__() without __hash__() has instances that cannot be used in hashable collections such as sets or dictionary keys: Python Data Model, “3. Data model”.

class Coordinate:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        if not isinstance(other, Coordinate):
            return NotImplemented
        return (self.x, self.y) == (other.x, other.y)

This value object is intentionally unhashable unless you add a hash consistent with equality and ensure the compared fields cannot change while it is in a set or dictionary. A frozen dataclass is often a convenient choice for immutable value records. Never base a hash on mutable fields that can change after insertion; doing so can make an object effectively disappear from its collection.

Compare only the state that defines identity

Do not include caches, timestamps, open handles, or other operational details in value equality unless they truly define the value. Return NotImplemented for unrelated types so Python can try the reflected comparison rather than claiming that unrelated objects are equal.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Make constructors and class methods communicate intent

Constructors should establish a valid minimum state, not perform a surprising application workflow. If creating an object requires I/O or remote authentication, consider a separate factory or an explicit connect() operation so callers can see the cost and failure point.

Use a @classmethod for an alternate constructor that returns an instance of the class, such as from_json or from_environment. Use a @staticmethod only when a helper is conceptually grouped with the class but needs neither instance nor class state. If a helper has no meaningful relationship to the type, a module-level function is easier to discover and test.

Design a class in a repeatable sequence

  1. Write the responsibility. State what the object owns and what it deliberately does not own.
  2. List state and lifetimes. Mark each value as per-instance, shared, immutable, or temporary. Move mutable per-instance values into initialization.
  3. State invariants. Identify valid ranges, required combinations, and transitions. Enforce them at construction and in mutating methods.
  4. Choose the relationship. Use a dataclass for a data-shaped value, inheritance for a genuine subtype, an ABC for an explicit contract, and delegation for a service relationship.
  5. Define the smallest public API. Expose intention-revealing operations rather than every internal field or collaborator.
  6. Choose equality semantics. Decide identity versus value comparison before implementing __eq__, and align hashing with that decision.
  7. Inject volatile dependencies. Pass clocks, gateways, repositories, and caches when replacing them in tests or deployments is useful.
  8. Test normal and boundary behavior. Include empty inputs, repeated calls, invalid transitions, collaborator failures, and independent instances.
  9. Review the design after a real use case. If callers keep reaching into internals, either add a focused method or reconsider whether the state belongs in this object.

Common OOP mistakes and their fixes

Symptom Underlying problem Better move
Instances unexpectedly share lists or dictionaries Mutable class attribute or default argument Create the value per instance with __init__ or default_factory
A deep hierarchy exists to reuse a few lines Inheritance chosen for code reuse rather than substitutability Extract a function, compose a collaborator, or use a focused mixin
Every caller checks several concrete types The behavioral contract is missing or scattered Define a small duck-typed interface, ABC, or delegated method
A “god object” knows storage, billing, formatting, and UI Responsibilities and state ownership are mixed Split collaborators and keep one workflow coordinator
Objects are dictionary keys but lookups fail after mutation Hash depends on mutable equality fields Use immutable value state or avoid hashing the object
Subclass initialization works only in one parent order Multiple inheritance is not cooperative Use compatible signatures and super(), or replace the hierarchy with delegation
Properties merely mirror public fields Encapsulation adds ceremony without protecting an invariant Use a direct field or dataclass until behavior justifies a property

Test the boundaries that make the design valuable

Unit tests should verify ownership as well as outputs. Construct two instances and mutate one; the other must remain unchanged unless shared state is intentional. Test invalid constructor values and invalid transitions to prove invariants are enforced at the object boundary.

For duck-typed collaborators, use a minimal fake that implements only the operations the consumer promises to call. This reveals accidental dependencies on concrete classes. For an ABC, test that incomplete implementations cannot be instantiated and that each concrete implementation honors the same observable contract.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

For delegated workflows, test both the successful call sequence and collaborator failures. The coordinator should translate or propagate errors deliberately, not leave partial state without a recovery policy. For value objects, test reflexive, symmetric, and transitive equality, and verify that equal objects behave correctly in sets or dictionaries when hashing is supported.

A compact review checklist

  • Can you name the object’s single primary responsibility?
  • Is every mutable value owned by the correct instance or explicit shared service?
  • Would a function, dictionary, tuple, or dataclass express the data more directly?
  • Is inheritance justified by a meaningful subtype relationship and a stable contract?
  • Would duck typing be sufficient, or does an ABC provide needed structure and shared behavior?
  • Are collaborators delegated to instead of being reimplemented or repeatedly type-checked?
  • Are invariants enforced through intentional methods rather than caller discipline?
  • Do equality and hashing match the object’s identity or value semantics?
  • Are multiple-inheritance methods cooperative and understandable from their MRO?
  • Can tests replace external services and prove that instances do not leak state?

The result should be a small, legible public surface with explicit ownership and relationships. Python gives you classes, inheritance, ABCs, and multiple inheritance, but elegance comes from using each only when it clarifies the behavior a reader must maintain.

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.