-
Notifications
You must be signed in to change notification settings - Fork 0
Love Your 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.
>>> 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)'
.
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!"
__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.
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.
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)
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
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.
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.
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
It is pronounced "kraypr" (/kɹeɪpr/) like Americans and Australians pronounce crêpe, the French pancake.
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.
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.
The code is tidy and clean, follows best practises, is fully tested and type checked as a hypermodern package should be.
You don't have to work in an unsafe environment when you contribute to this project.
It is still in its infancy, but you can install it from PyPI.
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: