The Ultimate Guide to Writing Clean Python Classes
The Art of Class Sizing and Separation
Writing effective @dataclass, serve as structured containers for information. Behavior-focused classes group related methods that perform actions.
By splitting a bloated Person class into specific components like Address and Stats, you improve readability and reusability. For instance, an EmailService shouldn't live inside a Person class; it belongs in its own module. This separation allows you to reuse the email logic across your entire application without dragging along unrelated personal data structures.

Enhancing Usability with Properties and Dunder Methods
Python offers powerful tools to make classes feel more "pythonic" and easier for other developers to use. Instead of writing verbose getter methods like get_bmi(), use the @property decorator. This allows users to access computed values as if they were simple attributes.
@property
def bmi(self) -> float:
return self.weight / (self.height ** 2)
To provide human-readable descriptions of your objects, implement the __str__ dunder method. While __repr__ for developers, a custom __str__ ensures that when a user prints an object, they see a clean, formatted string rather than a memory address or raw data dump.
Efficient Logic with Caching
If a property involves a heavy computation that doesn't change frequently, recomputing it every time it's accessed is wasteful. The @cached_property and @lru_cache.
@cached_property stores the result after the first access, but it can be risky if the underlying data changes. For more dynamic scenarios, @lru_cache (Least Recently Used cache) is superior for functions. It maps specific input arguments to their results, ensuring that if you call a function with the same parameters twice, the second call returns the cached result instantly.
Decoupling via Dependency Injection and Protocols
Hardcoding dependencies inside a class creates rigid code that is difficult to test. If a class needs an EmailService, don't instantiate it inside the __init__ or a method. Instead, inject it as an argument. To make this even more flexible, use a Protocol from the typing module.
from typing import Protocol
class EmailSender(Protocol):
def send_message(self, to: str, subject: str, body: str) -> None:
...
By defining a Protocol, your class only cares that the object it receives has a send_message method. It doesn't care if it's a real SMTP service or a mock object for testing. This architectural pattern, known as dependency inversion, is the key to building scalable software.
Knowing When Not to Use a Class
A common mistake in functools.partial to pre-fill function arguments, achieving the same configuration benefits as a class constructor without the overhead of object instantiation.
Encapsulation and Name Mangling
While Python doesn't have strict private access modifiers like Java, it uses conventions to signal internal use. A single underscore (e.g., _variable) is a hint to other developers that the attribute is internal. For stronger protection, a double underscore (e.g., __variable) triggers name mangling. This makes the attribute harder to access from outside the class, preventing accidental overrides in subclasses and keeping the public API clean.