Making friendly classes

Trey Hunner smiling in a t-shirt against a yellow wall
Trey Hunner
4 min. read Python 3.10—3.14
Share
Copied to clipboard.

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.

Always make your classes friendly

So you've decided to make a class. How can you make your class more user-friendly?

Friendly classes have these 3 qualities:

  1. They accept sensible arguments
  2. Instances have a nice string representation
  3. Instances can be sensibly compared to one another
  4. (Optionally) When it makes sense, they embrace dunder methods to overload functionality

An example friendly class

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.

Quick friendly classes with dataclasses

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 seem confusing?

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 all friendliness is the same

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:

  1. An initializer method: often one that accepts sensible arguments
  2. A nice string representation: often one that looks like Python code
  3. A sense of equality: how should these objects be comparable

Friendliness doesn't just stop with __init__, __repr__, and (often) __eq__.

Extra friendly classes

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

Use dataclasses as a measuring stick

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:

  1. Implement each of these traits yourself (define the dunder methods)
  2. Use a tool that does it for you (like dataclasses or attrs)
  3. Inherit from a class that already provides the functionality you need

Strive to write friendly classes, with or without dataclasses, attrs, and other class-creation helpers.

A Python Tip Every Week

Need to fill-in gaps in your Python skills? I send weekly emails designed to do just that.