Customizing the String Representation of your Objects

Related Topics:

Transcript:

Let's talk about how to customize the string representations of your Python objects.

The default string representation

We have a Point class here:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

We've made a new instance of this class and we're referring to that instance with the variable p. If we type p from the Python REPL, we see that it's a point.Point object at some memory location:

>>> p = Point(1, 2)
>>> p
<point.Point object at 0x7f128f519ee0>

If we print it we see the same thing:

>>> print(p)
<point.Point object at 0x7f128f519ee0>

And if we convert to a string explicitly, we see the same thing:

>>> str(p)
'<point.Point object at 0x7f128f519ee0>'

Explicit string conversions use __str__

It would be nice to see something, besides the module name and the class name of this object. Maybe the data that's actually stored in this object. Let's create a __str__ method on this Point class (pronounced "dunder str", dunder meaning "double underscore"):

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __str__(self):
        return f"({self.x}, {self.y})"

This will customize what happens when we pass the Point object to the built-in str function:

>>> p = Point(1, 2)
>>> str(p)
'(1, 2)'
>>> p.__str__()
'(1, 2)'

The built-in str function actually calls the __str__ method on the object that we give it.

In fact, if we print something out or otherwise, implicitly convert something to a string, it does the same thing: it calls that __str__ method.

>>> print(p)
(1, 2)

Programmer-readable string conversions use __repr__

We're not done yet! Our Python object does not yet have a nice string representation in all cases.

If we type p from the Python REPL, we still see the point.Point object at some memory location:

>>> p
<point.Point object at 0x7f7e54f46be0>

The REPL actually doesn't use, the str built-in function, it uses the built-in repr function.

>>> repr(p)
'<point.Point object at 0x7f7e54f46be0>'

And the built-in repr function relies on the __repr__ method:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __repr__(self):
        return f"Point({self.x}, {self.y})"
    def __str__(self):
        return f"({self.x}, {self.y})"

By making a __repr__ method, we've customized what happens when you call repr on our Point objects:

>>> p = Point(1, 2)
>>> repr(p)
'Point(1, 2)'

And by making a __str__ method, we've customized what happens when you call str:

>>> str(p)
'(1, 2)'

And so when we just type p or when we print(p), we'll get friendly string representations:

>>> p
Point(1, 2)
>>> print(p)
(1, 2)

So, we've customized the programmer-readable string representation for object (which by convention looks like Python code) and a human readable string representation for our object, which is what an end user might expect to see.

You really only need __repr__

Almost every Python object only customizes one of these: __repr__, the programmer readable string representation.

If we just define a __repr__ method on our class:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __repr__(self):
        return f"Point({self.x}, {self.y})"

It will customize what programmers see:

>>> p = Point(1, 2)
>>> p
Point(1, 2)

But it also customizes what happens when we print out our object:

>>> print(p)
Point(1, 2)

On when we convert it to a string explicitly:

>>> str(p)
'Point(1, 2)'

This happens because the default __str__ method on all Python objects delegates to the __repr__ method.

>>> p.__str__()
'Point(1, 2)'

Summary

When you're defining your own classes in Python, always make a __repr__ method.

If you want an additional human-readable string representation, you might wanna customize the __str__ method, but you *at least& need to customize the __repr__ method of your Python objects.