In Part 8 we introduced the concept of dunder methods and mentioned how they are used "under the hood" to perform much of the day-to-day business logic Python does behind the scenes. In this Part we will delve a little into a simple example of how to leverage two in particular.
In all classes there are two simple dunder methods called __str__
which is called whenever a class is turned into a string (hence the str
) and __repr__
which is called whenever a representation of the class is needed. This is typically when a visual representation of the class is needed (this may sound similar to __str__
but it is subtly not as will be explained shortly). Each just returns a string, usually based on what's in the class. What's the difference? Well it's actually quite simple. The repr
is for when something is returned visually but not printed. For example, if you are doing python in an interactive interpreter and output an object you will see that object's repr
, but if you print the object with the print()
function you will see the object's str
.
There are two rules that come out of this.
- The
__str__
method should be a useful visual representation of the class instance - The
__repr__
method should show what the class is, and ideally how to create this instantiation yourself. Ideally, it should be evaluatable code.
For example, below is a class I used for Advent of Code 2024 Day 10. The code snippet actually contains two classes, Tile
and Point
. Dataclasses in Python actually provide a built-in str
and repr
function that renders exactly what the class is and what each of the properties are.
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
@dataclass
class Tile:
point: Point
height: str
tile = Tile(Point(0, 0), '0')
print(tile)
Printing out the instance of tile
produces this output.
Tile(point=Point(x=0, y=0), height='0')
This is a perfect repr
because if you were to copy and paste that code into the program it would create the object it came from. But as a str
method result? It's quite verbose. However in the challenge we're going to have a lot of these tiles and this is unnecessarily verbose for the output too. What if instead of one we had to print out 500?
We can create a str
method instead that makes the output more compact and to do so, we only need to add two lines to our existing script.
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
@dataclass
class Tile:
point: Point
height: str
def __str__(self) -> str:
return f'(x:{self.point.x}-y:{self.point.y}-h:{self.height})'
tile = Tile(Point(0, 0), '0')
print(tile)
Now if we print tile
we see the following output, which is a lot shorter and keeps the same amount of information available.
(x:0-y:0-h:0)
This is a pretty simple trick but the benefits are significant and can make UX and debugging significantly easier. Next time, in Part 10, we're going to tackle a topic I've been putting off, because it's a tricky subject, and we're finally ready to talk about it: inheritance.
Take a Step Back?
If you haven't already read the previous articles in this series, you may want to take a step back to look at some of the information that has brought us to this point. For a complete look at the articles in this series, you can visit the overview page: Getting started with classes in Python
Or, if you want to look directly at a particular previous article, here are the links: