7

Summary

I have a dataclass with 10+ fields. print()ing them buries interesting context in a wall of defaults - let's make them friendlier by not needlessly repeating those.

Dataclasses in Python

Python's @dataclasses.dataclass() (PEP 557) provides automatic printable representations (__repr__()).

Assume this example, based on python.org's:

from dataclasses import dataclass


@dataclass
class InventoryItem:
    name: str
    unit_price: float = 1.00
    quantity_on_hand: int = 0

The decorator, through @dataclass(repr=True) (default) will print() a nice output:

InventoryItem(name='Apple', unit_price='1.00', quantity_on_hand=0)

What I want: Skip printing the defaults

repr It prints all the fields, including implied defaults you wouldn't want to show.

print(InventoryItem("Apple"))

# Outputs: InventoryItem(name='Apple', unit_price='1.00', quantity_on_hand=0)
# I want: InventoryItem(name='Apple')
print(InventoryItem("Apple", unit_price="1.05"))

# Outputs: InventoryItem(name='Apple', unit_price='1.05', quantity_on_hand=0)
# I want: InventoryItem(name='Apple', unit_price='1.05')
print(InventoryItem("Apple", quantity_on_hand=3))

# Outputs: InventoryItem(name='Apple', unit_price=1.00, quantity_on_hand=3)
# I want: InventoryItem(name='Apple', quantity_on_hand=3)
print(InventoryItem("Apple", unit_price='2.10', quantity_on_hand=3))

# Output is fine (everything's custom):
# InventoryItem(name='Apple', unit_price=2.10, quantity_on_hand=3)

Discussion

Internally, here's the machinery of dataclass repr-generator as of python 3.10.4: cls.__repr__=_repr_fn(flds, globals)) -> _recursive_repr(fn)

It may be the case that @dataclass(repr=False) be switched off and def __repr__(self): be added.

If so, what would that look like? We don't want to include the optional defaults.

Context

To repeat, in practice, my dataclass has 10+ fields.

I'm print()ing instances via running the code and repl, and @pytest.mark.parametrize when running pytest with -vvv.

Big dataclass' non-defaults (sometimes the inputs) are impossible to see as they're buried in the default fields and worse, each one is disproportionately and distractingly huge: obscuring other valuable stuff bring printed.

Related questions

As of today there aren't many dataclass questions yet (this may change):

tony
  • 870
  • 7
  • 16

1 Answers1

6

You could do it like this:

import dataclasses
from dataclasses import dataclass
from operator import attrgetter


@dataclass(repr=False)
class InventoryItem:
    name: str
    unit_price: float = 1.00
    quantity_on_hand: int = 0

    def __repr__(self):
        nodef_f_vals = (
            (f.name, attrgetter(f.name)(self))
            for f in dataclasses.fields(self)
            if attrgetter(f.name)(self) != f.default
        )

        nodef_f_repr = ", ".join(f"{name}={value}" for name, value in nodef_f_vals)
        return f"{self.__class__.__name__}({nodef_f_repr})"
        

# Prints: InventoryItem(name=Apple)
print(InventoryItem("Apple"))

# Prints: InventoryItem(name=Apple,unit_price=1.05)
print(InventoryItem("Apple", unit_price="1.05"))

# Prints: InventoryItem(name=Apple,unit_price=2.10,quantity_on_hand=3)
print(InventoryItem("Apple", unit_price='2.10', quantity_on_hand=3))
tony
  • 870
  • 7
  • 16
user2246849
  • 4,217
  • 1
  • 12
  • 16
  • 1
    Nice! I wonder if `repr=False` is really necessary (and can't test myself right now) – wouldn't defining `__repr__` override the built-in method anyway? – fsimonjetz May 08 '22 at 13:48
  • 1
    Yes, it is not necessary as you said. However, I left it there as a "reminder" that we actually replaced `__repr__` – user2246849 May 08 '22 at 13:49
  • @user2246849 This works! Thank you. Are you willing to additionally license the code in your answer under MIT or [Python license](https://docs.python.org/3/license.html#bsd0)? I'd like to use it (I can still give attribution and link to your answer as well) – tony May 08 '22 at 13:59
  • 1
    @TonyN yes of course, feel free to use it as you wish. I added a license snippet to my profile. – user2246849 May 08 '22 at 15:25
  • 1
    @user2246849 Thanks for your reply and clarification! That's a fair point, especially if the `def __repr__` is buried somewhere deep down in a more complex class. – fsimonjetz May 08 '22 at 15:26
  • 1
    Here is your answer in action: Definition [`libvcs.utils.dataclasses.SkipDefaultFieldsMixin`](https://github.com/vcs-python/libvcs/blob/v0.13.0a3/libvcs/utils/dataclasses.py#L5) and [usage](https://github.com/vcs-python/libvcs/blob/v0.13.0a3/libvcs/utils/subprocess.py#L71) + [docs](https://libvcs.git-pull.com/internals/dataclasses.html). I added doctests. When using it as a subclass I need to pass `repr=False` to `@dataclasses.dataclass`. When using `__repr__` directly it can be omitted. – tony May 08 '22 at 21:21