Thanks to visit codestin.com
Credit goes to realpython.com

Classes

Classes let you bundle related data and behavior into a single, coherent unit. They’re fundamental tools for modeling real-world concepts and domain ideas in Python code.

Well-designed classes make it easier to understand how data flows through your code, reuse behavior, and grow your codebase over time. Poorly designed classes quickly become difficult to reason about and modify.

When designing classes, these best practices encourage clear responsibilities and reusable designs:

  • Give each class a single, clear responsibility or reason to change. Write classes that model one concept or role in your domain rather than doing many different things. If a class continues to grow and becomes too broad, consider splitting it into smaller, more focused classes.
  • Be cautious with very small classes. If a class has only one method and doesn’t represent a meaningful concept or abstraction, a regular function in a module is often simpler and clearer. That said, small “service” or “policy” classes can be appropriate when they model a distinct role.
  • Favor data classes for storing data with minimal or no behavior. When a class mainly stores data and has a few simple behaviors, use a data class with appropriate fields. These classes save you from writing boilerplate code, such as .__init__() and .__repr__(). They also keep your intent clear: storing data.
  • Prefer composition over inheritance. Use inheritance sparingly for unambiguous is-a relationships. For most use cases, it’s safer to write small classes and delegate work to them rather than building complicated inheritance hierarchies that are hard to change or reason about.
  • Use properties to add function-like behavior to attributes without breaking your API. If users of your code currently access an attribute directly, and you need to add function-like behavior on top of it, convert the attribute into a property. This practice prevents you from breaking your API, allowing users to keep accessing obj.attr instead of forcing them to change to something like obj.get_attr().
  • Leverage special methods when they improve usability. Methods like .__str__(), .__repr__(), .__len__(), .__iter__(), and others let your classes behave like built-in types.
  • Apply the SOLID principles pragmatically. The SOLID principles—Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion—can help guide object-oriented design, but they should support clarity and maintainability rather than being followed rigidly.

To see these practices in action, consider the Article class below.

🔴 Avoid this:

Python
class Article:
    def __init__(self, title, body, tags, db):
        self.title = title
        self.body = body
        self.tags = tags or []
        self.db = db
        self.slug = None
        self.published = False

    def publish(self):
        if self.slug is None:
            self.slug = "-".join(self.title.lower().split())

        self.db.save_article(
            title=self.title,
            body=self.body,
            tags=self.tags,
            slug=self.slug,
        )

        self.published = True

This class mixes several responsibilities:

  • Generating a slug
  • Storing the article by talking directly to the database
  • Keeping mutable state (.slug, .published) that depends on calling .publish() in the right order

Over time, this kind of class tends to accumulate more behavior and side effects, making it harder to understand, test, and reuse.

Favor this:

Python
from dataclasses import dataclass, field
from datetime import datetime, timezone

def utcnow() -> datetime:
    return datetime.now(timezone.utc)

@dataclass
class Article:
    title: str
    body: str
    tags: list[str] = field(default_factory=list)
    created_at: datetime = field(default_factory=utcnow)
    published_at: datetime | None = None

    @property
    def is_published(self) -> bool:
        return self.published_at is not None

    @property
    def slug(self) -> str:
        return "-".join(self.title.lower().split())

    def __str__(self) -> str:
        status = "published" if self.is_published else "draft"
        return f"{self.title} [{status}]"

class Publisher:
    def __init__(self, db):
        self._db = db

    def publish(self, article: Article) -> None:
        if article.is_published:
            return
        article.published_at = datetime.now(timezone.utc)
        self._db.save(article)

In this version, you use a data class to model the Article. It holds the core data and exposes derived information, such as publishing status and slug, via properties. It also includes a custom .__str__() method to provide a user-friendly string representation for Article instances.

The publishing responsibility is moved into a separate Publisher class, which represents a distinct role in the system and depends on a database provided via composition. This separation makes each class easier to test, reason about, and change independently.

Tutorial

Python Classes: The Power of Object-Oriented Programming

In this tutorial, you'll learn how to create and use full-featured classes in your Python code. Classes provide a great way to solve complex programming problems by approaching them through models that represent real-world objects.

intermediate python

For additional information on related topics, take a look at the following resources:


By Leodanis Pozo Ramos • Updated Dec. 23, 2025 • Reviewed by Brenda Weleschuk and Bartosz Zaczyński