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.attrinstead of forcing them to change to something likeobj.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:
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:
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.
Related Resources
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.
For additional information on related topics, take a look at the following resources:
- Object-Oriented Programming (OOP) in Python (Tutorial)
- Inheritance and Composition: A Python OOP Guide (Tutorial)
- Python Class Constructors: Control Your Object Instantiation (Tutorial)
- Providing Multiple Constructors in Your Python Classes (Tutorial)
- Data Classes in Python (Guide) (Tutorial)
- Python's Magic Methods: Leverage Their Power in Your Classes (Tutorial)
- Python's property(): Add Managed Attributes to Your Classes (Tutorial)
- Python Descriptors: An Introduction (Tutorial)
- SOLID Design Principles: Improve Object-Oriented Code in Python (Tutorial)
- What Are Mixin Classes in Python? (Tutorial)
- Custom Python Dictionaries: Inheriting From dict vs UserDict (Tutorial)
- Custom Python Lists: Inheriting From list vs UserList (Tutorial)
- Custom Python Strings: Inheriting From str vs UserString (Tutorial)
- Python's Instance, Class, and Static Methods Demystified (Tutorial)
- Python's .__call__() Method: Creating Callable Instances (Tutorial)
- Supercharge Your Classes With Python super() (Tutorial)
- Operator and Function Overloading in Custom Python Classes (Tutorial)
- Inheritance and Internals: Object-Oriented Programming in Python (Course)
- Class Concepts: Object-Oriented Programming in Python (Course)
- Python Classes - The Power of Object-Oriented Programming (Quiz)
- Intro to Object-Oriented Programming (OOP) in Python (Course)
- Object-Oriented Programming (OOP) in Python (Quiz)
- Inheritance and Composition: A Python OOP Guide (Course)
- Inheritance and Composition: A Python OOP Guide (Quiz)
- Using Python Class Constructors (Course)
- Python Class Constructors: Control Your Object Instantiation (Quiz)
- Using Multiple Constructors in Your Python Classes (Course)
- Using Data Classes in Python (Course)
- Data Classes in Python (Quiz)
- Python's Magic Methods in Classes (Course)
- Python's Magic Methods: Leverage Their Power in Your Classes (Quiz)
- Managing Attributes With Python's property() (Course)
- Python's property(): Add Managed Attributes to Your Classes (Quiz)
- Python Descriptors (Course)
- Design and Guidance: Object-Oriented Programming in Python (Course)
- SOLID Design Principles: Improve Object-Oriented Code in Python (Quiz)
- What Are Mixin Classes in Python? (Quiz)
- OOP Method Types in Python: @classmethod vs @staticmethod vs Instance Methods (Course)
- Python's Instance, Class, and Static Methods Demystified (Quiz)
- Supercharge Your Classes With Python super() (Course)
- Supercharge Your Classes With Python super() (Quiz)