Skip to content

Love Your Representation

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

A dinosaur in the vaults of paranassus

Embrace the power of __repr__

Python Representation

Usage

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.

>>> class Simple:
...     def __init__(self, name):
...             self.name = name
...     def __str__(self):
...             return f'My name is {self.name}'
...     def __repr__(self):
...             return f'Simple({self.name})'
... 
>>> simple = Simple('Christian')
__str__ __repr__
print(simple) repr(simple)
'%s' % simple '%r' % simple
'{}'.format(simple) '{!r}'.format(simple)
f'{simple}' f'{simple!r}'
Output
'My name is Christian' 'Simple(Christian)'

F-Srings in newer Python versions also provide a nice shortcut: f'{simple=}' will produce 'simple=Simple(Christian)'.

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

Why should I care about implementing a __repr__?

Implementing a __repr__ method for your Python classes can be valuable for several reasons, particularly in debugging, development, and logging:

  • Debugging: When you're debugging your code, having a well-defined __repr__ method can provide you with a clear and informative representation of an object's state. This can help you quickly identify issues and understand the data stored in an object, making debugging more efficient.
  • Development: During development, you may frequently need to inspect the contents of objects for testing and problem-solving. A useful __repr__ method can simplify this process by displaying relevant object details, helping you develop and fine-tune your code more effectively.
  • Logging: When you log information about your program's execution, having a good __repr__ method can be invaluable. Logging the string representation of objects can help you track the program's state, record important data, and diagnose issues when they occur.
  • Readable Output: A well-implemented __repr__ method can provide human-readable output, making it easier to understand the state of objects. This is especially useful when you need to inspect the program's behavior during development or debugging.
  • Interactive Environments: In interactive Python environments like Jupyter notebooks, IPython, and REPL, the __repr__ method is used automatically when you evaluate an object. This makes it convenient to understand and explore the object's content without explicitly calling the __repr__ method.
  • Documentation and Exploration: When working with libraries and frameworks, having a descriptive __repr__ can enhance the documentation and help others understand how to use your classes effectively. It also makes it easier for colleagues or users to explore and experiment with your code.

Implementing a meaningful __repr__ method is a good practice because it provides a clear and informative way to represent your objects, making your code more maintainable, debuggable, and user-friendly. It's especially useful when you need to interact with and understand objects during the development and debugging process.

Inspection of a python

Examples From The Python Standard Library

An 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

Even in the Python Standard Library the implementation of __repr__ may vary.

>>> from enum import Enum
>>> 
>>> class DataType(Enum):
...     string = "string"
...     int_ = "int"
...     uint = "uint"
...     short = "short"
... 
>>> ui = DataType('uint')
>>> ui
<DataType.uint: 'uint'>

This is not undisputed and 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

Example

fastkml is a library to work with <KML /> a geospatial <XML /> format.

>>> 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>
... """

Out of the box, python provides an unambiguous representation.

>>> track = Track.from_string(doc, ns="")
<fastkml.gx.Track object at 0x7f3d3b5fa230>
>>> 

That is not much help.

The __init__ method of this class is defined 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:

When we define the __repr__ like

    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 representation looks much better

>>> track = Track.from_string(doc, ns="")
>>> track
Track(
    ns="http://www.google.com/kml/ext/2.2",
    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),
        ),
    ],
)

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 "kraypr" (/kɹeɪpr/) like Americans and Australians pronounce crêpe, the French pancake.

XKCD crepe

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.

Example

Given the file tests/classes/kw_only_test.py with the contents:

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

The command:

❯ crepr tests/kw_only_test.py

produces

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

    def __repr__(self) -> str:
        """Create a string (c)representation for KwOnly."""
        return (f'{self.__class__.__module__}.{self.__class__.__name__}('
            f'name={self.name!r}, '
            f'age={self.age!r}, '
        ')')

The repr() of an instance of this class will be:

>>> from tests.classes.kw_only_test import KwOnly
>>> kwo = KwOnly('Christian', age=25)
>>> kwo
tests.classes.kw_only_test.KwOnly(name='Christian', age=25, )

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

Summary

Implementing a __repr__ method is a best practice as it improves the debugging and development process, enhances code readability, and aids in effective communication of your object's structure and state. Do not forget to include the !r qualifier when logging your objects.

Give your representations some love.

❤️.__repr__(self) -> str:

Love your repr

Links