Unlike many programming languages, in Python we don't really need to invent our own classes. We can invent our own classes, but we don't strictly need to.
Instead of classes, we could store all our data within lists and dictionaries and store all our functionality in functions. Often, especially when your code is relatively simple, not using a class makes for more readable code.
Of course, at some point in your Python coding journey, you will probably find some opportunities to make classes. When you do make a class, make sure it's a friendly class.
So you've decided to make a class. How can you make your class more user-friendly?
Friendly classes have these 3 qualities:
Here's a fairly friendly Point class:
class Point:
def __init__(self, x, y, z):
self.x, self.y, self.z = x, y, z
def __repr__(self):
class_name = type(self).__name__
return f"{class_name}(x={self.x!r}, y={self.y!r}, z={self.z!r})"
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return (self.x, self.y, self.z) == (other.x, other.y, other.z)
I don't call this class friendly because of how its implementation looks. This class is friendly because of how it behaves.
Friendly classes accept sensible arguments:
>>> p = Point(1, 2, 3)
Friendly classes have a nice string representation:
>>> p
Point(x=1, y=2, z=3)
And friendly classes support sensible equality checks:
>>> q = Point(1, 2, 3)
>>> p == q
True
This all works thanks to our __init__, __repr__, and __eq__ methods.
Want to make a friendly class quickly? Try using Python's dataclasses.dataclass decorator.
Here's the same class as above, implemented using a dataclass:
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
z: float
Just like any other class, you can add your own methods and properties to dataclasses.
For example, we could add a method that computes the distance to another point:
from dataclasses import dataclass
from math import sqrt
@dataclass
class Point:
x: float
y: float
z: float
def distance_to(self, other):
return sqrt(
(self.x - other.x)**2
+ (self.y - other.y)**2
+ (self.z - other.z)**2
)
>>> p = Point(1, 2, 3)
>>> q = Point(4, 5, 6)
>>> p.distance_to(q)
5.196152422706632
Dataclasses are an additional abstraction on top of Python's regular class syntax.
All abstractions come with trade-offs: some functionality is automatic, which means we get it for free, but it's also hidden from us.
If you ever wonder what it would take to write a class that's equivalent to a given dataclass, try my dataclass to regular class converter at https://pym.dev/undataclass.
That undataclass tool can be helpful for better appreciating dataclasses. It may also be helpful for writing better classes more quickly, even if you decide not to use a dataclass.
Not every class needs a __eq__ method.
Some objects really don't need to be comparable to other objects with equality.
For example, it wouldn't make much sense to compare two open database connections or two logging objects for equality. For objects like these, identity (whether they're the exact same object) matters more than value equality (whether they represent the same data).
And not all objects even need a friendly string representation.
If you're making an object that has no sensible state to display, you could leave __repr__ undefined.
Python takes this approach with its many iterator objects:
>>> enumerate([])
<enumerate object at 0x76dd66978fe0>
But most classes would benefit from these 3 traits:
Friendliness doesn't just stop with __init__, __repr__, and (often) __eq__.
The __init__, __repr__, and __eq__ methods are the most common dunder methods, but Python's classes support over 100 other dunder methods.
If your class would benefit from one of Python's many other dunder methods, you should implement them. Don't go overboard though: only implement dunder methods that really make sense for your class!
For example, we could make our Point objects support tuple unpacking (and other forms of iterability) by adding a __iter__ method:
from dataclasses import astuple, dataclass
@dataclass
class Point:
x: float
y: float
z: float
def __iter__(self):
return iter(astuple(self))
This would allow our Point objects to work with tuple unpacking:
>>> p = Point(1, 2, 3)
>>> x, y, z = p
>>> print(x, y, z)
1 2 3
Don't overuse classes. But when you do write your own classes, make your classes friendly. Users of your custom classes will enjoy using your classes more when they're friendly.
Dataclasses allow us to create friendly classes while writing less boilerplate code.
Third-party libraries, like attrs, provide even more feature-rich class-creation helpers than the dataclasses module.
Even if you don't use them directly, dataclasses are a good measuring stick for your class-based code. The next time you create a class, be sure to ask yourself, "is my class at least as friendly as an equivalent dataclass?"
For each new class you make, either:
dataclasses or attrs)Strive to write friendly classes, with or without dataclasses, attrs, and other class-creation helpers.
Need to fill-in gaps in your Python skills?
Sign up for my Python newsletter where I share one of my favorite Python tips every week.
Need to fill-in gaps in your Python skills? I send weekly emails designed to do just that.
Sign in to your Python Morsels account to track your progress.
Don't have an account yet? Sign up here.