Skip to content

Love Your Representation

Christian Ledermann edited this page Nov 4, 2023 · 39 revisions

A dinosaur in the vaults of paranassus

Embrace the power of __repr__

Python Representation

There are two ways out that Python provides us with, out of the box, to convert an object into a string: __str__ and __repr__. __str__ gets all the love, __repr__ is a bit more obscure.

Python loves string

What is the difference anyway?

  • __str__ is meant to define the "informal" or user-friendly string representation of an object. It's used when you want to provide a human-readable description of the object.
  • __repr__ is meant for the "formal" or unambiguous string representation of an object. It's used for debugging, development, and to represent the object in a way that could be used to recreate the same object.

Think of a Python object as a toy. When we want to describe this toy to someone, we can use two ways:

__str__ is like giving a friendly and simple description of the toy. It's what we tell our friends when we want them to know what the toy looks like or what it does. It's like saying, "This toy is colourful and fun to play with!"

Python toy

__repr__ is like giving a very detailed and technical description of the toy. It's what we tell someone who needs to know all the nitty-gritty details about the toy. A look under the hood.

Tech Python

Out of the box, python provides an unambiguous representation.

>>> class Simple:
...     pass
... 
>>> simple = Simple()
>>> simple
<__main__.Simple object at 0x7f3d3b5fa230>
>>> 

That is not much help.

A better example is the datetime module.

>>> import datetime
>>> now = datetime.datetime.utcnow()
>>> now
datetime.datetime(2023, 11, 3, 18, 55, 45, 862092)
>>> 

Data classes provide a nice implementation.

>>> from dataclasses import dataclass
>>> 
>>> @dataclass
... class InventoryItem:
...     """Class for keeping track of an item in inventory."""
...     name: str
...     unit_price: float
...     quantity_on_hand: int = 0
... 
>>> ii = InventoryItem('python', 897)
>>> ii
InventoryItem(name='python', unit_price=897, quantity_on_hand=0)

Weird python

In the Python Standard Library, we also find some weird examples

>>> from enum import Enum
>>> 
>>> class DataType(Enum):
...     string = "string"
...     int_ = "int"
...     uint = "uint"
...     short = "short"
...     ushort = "ushort"
...     float_ = "float"
...     double = "double"
...     bool_ = "bool"
... 
>>> ui = DataType('uint')
>>> ui
<DataType.uint: 'uint'>

But that can be overridden (this is copied from: "3.12.0 Documentation » The Python Standard Library » Data Types » enum — Support for enumerations")

>>> from enum import auto
>>> class OtherStyle(Enum):
...     ALTERNATE = auto()
...     OTHER = auto()
...     SOMETHING_ELSE = auto()
...     def __repr__(self):
...         cls_name = self.__class__.__name__
...         return f'{cls_name}.{self.name}'
... 
>>> OtherStyle.ALTERNATE
OtherStyle.ALTERNATE

Polished python

A more polished example is fastkml

>>> from fastkml.gx import Track
>>> doc = """
...     <gx:Track xmlns:gx="http://www.google.com/kml/ext/2.2"
...         xmlns:kml="http://www.opengis.net/kml/2.2">
...         <kml:when>2010-05-28T02:02:09Z</kml:when>
...         <kml:when>2010-05-28T02:02:56Z</kml:when>
...         <kml:when />
...         <gx:angles>45.54 66.23 77.0</gx:angles>
...         <gx:angles />
...         <gx:angles>75.54 86.23 17.0</gx:angles>
...         <gx:coord>-122.20 37.37 156.00</gx:coord>
...         <gx:coord>-122.20 37.37 152.00</gx:coord>
...         <gx:coord>-122.20 37.37 147.00</gx:coord>
...     </gx:Track>
... """
>>> track = Track.from_string(doc, ns="")
>>> track
Track(
    ns="",
    id="",
    target_id="",
    extrude=None,
    tessellate=None,
    altitude_mode=None,
    track_items=[
        TrackItem(
            when=datetime.datetime(2010, 5, 28, 2, 2, 9, tzinfo=tzutc()),
            coord=Point(-122.2, 37.37, 156.0),
            angle=Angle(heading=45.54, tilt=66.23, roll=77.0),
        ),
        TrackItem(
            when=datetime.datetime(2010, 5, 28, 2, 2, 56, tzinfo=tzutc()),
            coord=Point(-122.2, 37.37, 152.0),
            angle=None,
        ),
        TrackItem(
            when=None,
            coord=Point(-122.2, 37.37, 147.0),
            angle=Angle(heading=75.54, tilt=86.23, roll=17.0),
        ),
    ],
)

This is implemented as

class Track(_Geometry):
    """A track describes how an object moves through the world over a given time period."""
    def __init__(
        self,
        *,
        ns: Optional[str] = None,
        id: Optional[str] = None,
        target_id: Optional[str] = None,
        extrude: Optional[bool] = False,
        tessellate: Optional[bool] = False,
        altitude_mode: Optional[AltitudeMode] = None,
        track_items: Optional[Sequence[TrackItem]] = None,
    ) -> None:
         ...
    def __repr__(self) -> str:
        return (
            f"{self.__class__.__name__}("
            f"ns={self.ns!r}, "
            f"id={self.id!r}, "
            f"target_id={self.target_id!r}, "
            f"extrude={self.extrude!r}, "
            f"tessellate={self.tessellate!r}, "
            f"altitude_mode={self.altitude_mode}, "
            f"track_items={self.track_items!r}"
            ")"
        )

The pattern is simple, construct a string out of the objects attributes for each parameter in the __init__ method.

This could be the end. Just add a __repr__ that mirrors the __init_ signature of the class. There is nothing much to it, easily done and it will save you time and effort in the future when you have to debug your code or figure out what caused a bug from the log files.

The End?

Implementing this pattern to your existing code looks like a lot of tedious, error-prone, ungrateful, repetitive, manual labour. It is a major chore when your project has dozens of classes, let alone hundreds or thousands.

Manual labour

Being a developer, I’m too lazy to spend 8 hours mindlessly performing a function, but not too lazy to spend 16 hours automating it. Timothy Crosley

A Python coding

It is pronounced like Americans and Australians pronounce crêpe, the French pancake.

crepe

A Python script that takes a file name as a command-line argument, imports the specified module, and then prints a __repr__ method for each class defined in the module. It creates the __repr__ by inspecting the keyword arguments of the __init__ method of the class.

❯ crepr --help
Usage: crepr [OPTIONS] FILE_NAME

  Create a __repr__ method for each class in the specified file.

Arguments:
  FILE_NAME  [required]

Options:
  --help                          Show this message and exit.

For a class definition like

class KwOnly:

    def __init__(self, name: str, *, age: int) -> None:
        """Initialize the class."""
        self.name = name 
        self.age = age  

The output is

class KwOnly:
    """The happy path class."""

    def __init__(self, name: str, *, age: int) -> None:
        """Initialize the class."""
        self.name = name 
        self.age = age 
 
    def __repr__(self) -> str:
        """Return the string (c)representation of KwOnly"""
        return (f'{self.__class__.__name__}('
            f'name={self.name!r}, '
            f'age={self.age!r}, '
        ')')

It is Unix-ish, does just one thing: it prints the modified file to StdOut so you can redirect or pipe the output.

Unix python

The code is tidy and clean, follows best practises, is fully tested and type checked as a hypermodern package should be.

hyper modern kitchen

You don't have to work in an unsafe environment when you contribute to this project.

burning kitchen

It is still in its infancy, but you can install it from PyPI.

Baby python

That is it, Give your representations some love.

❤️.__repr__(self) -> str:

Love your repr