Locked learning resources

Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Locked learning resources

This lesson is for members only. Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Data Classes for Representation

00:00 Representation. Recall that we can easily create an entire deck of cards. While this representation of a Deck is explicit and kind of readable, it’s also extremely verbose. As you can see here, the entire Deck takes up most of the screen.

00:22 Let’s add a more concise representation. In general, a Python object has two different string representations: The repr of the object is defined by the .__repr__() method and should return a developer-friendly representation of the object. If possible, this should be code that can recreate the object. Data classes do this.

00:45 The string representation is defined by the .__str__() method and should return a user-friendly representation of the object. Data classes don’t implement the .__str__() method by default, so Python falls back to the .__repr__() method already seen.

01:03 Let’s implement a user-friendly representation of a PlayingCard.

01:38 The cards now look much nicer, but the deck is still as verbose as ever. To show that it’s possible to add your own .__repr__() method as well, we will violate the principle that it should return code that can recreate an object. Practicality beats purity, after all. The following code adds a more concise representation of the Deck.

02:43 Note, the !s specifier in the format string. It means that we explicitly want to use the str() string representation of each PlayingCard. With the new .__repr__(), the representation of the Deck is easier on the eyes, as seen here.

03:06 Now that you know how to alter the appearance of data classes, in the next section, you’ll see how to allow comparisons between instances of a data class.

Avatar image for dotnet

dotnet on Nov. 16, 2022

hi, another really outstanding course. my question relates to the section ‘Data Classes for Representation’. I did not understand, why you add your own .repr() method to deck and violate the principle that it should return code that can recreate an object. Why not add an appropriate .str method instead ?

Thanks a lot

Avatar image for Geir Arne Hjelle

Geir Arne Hjelle RP Team on Nov. 16, 2022

Hi @dotnet, there are a few more details about this example in the written tutorial: realpython.com/python-data-classes/#you-need-representation

As you note, the better choice is usually to reserve .__repr__() for a representation that can recreate an object.

However, these cards are typically handled as a list of cards (i.e. a deck of cards) rather than individually. When you call str() on a regular Python list, it will use repr() on each individual element. So changing .__str__() of a card doesn’t help the representation of a deck.

The best way to get a decent string representation of a deck of cards is probably to define a custom list class representing a deck, maybe with collections.UserList. However, a simpler, more pragmatic approach, was chosen here: prioritize str() of a deck of cards over repr() of an individual card.

Avatar image for dotnet

dotnet on Nov. 17, 2022

hi,

me again :-).

in my programm i use the following Deck definition (using .str() instead of .repr) and it works:

@dataclasses.dataclass class Deck: cards: list[PlayingCard] = dataclasses.field(default_factory=make_french_deck)

def __str__(self) -> str:
    cards = ', '.join(f'{c!s}' for c in self.cards)
    return f'{self.__class__.__name__}({cards})'
Avatar image for Geir Arne Hjelle

Geir Arne Hjelle RP Team on Nov. 17, 2022

Hi again, @dotnet :)

Yes, I agree. That is a more pythonic implementation. It works but it leaves the representation of Deck very verbose (which it needs to be in order to be code that reproduces the deck).

As noted in the text, I opted to go for a solution that’s more convenient to play with interactively. Whether this is a good idea or not completely depends on your problem at hand as well as your preferred workflow.

The differences are on the margins anyway. For example, by keeping the implementation on .__str__() instead of .__repr__() you need to do something to get the compact representation. For example, calling str():

>>> str(Deck())
'Deck(♣2, ♣3, ♣4, ♣5, ♣6, ♣7, ♣8, ♣9, ♣10, ♣J, ♣Q, ♣K, ♣A, ♦2, ♦3, ♦4, ♦5, ♦6, ♦7, ♦8, ♦9, ♦10, ♦J, ♦Q, ♦K, ♦A, ♥2, ♥3, ♥4, ♥5, ♥6, ♥7, ♥8, ♥9, ♥10, ♥J, ♥Q, ♥K, ♥A, ♠2, ♠3, ♠4, ♠5, ♠6, ♠7, ♠8, ♠9, ♠10, ♠J, ♠Q, ♠K, ♠A)'

If you never use the capability of recreating a deck from its repr, then it might be worth it to override .__repr__() and avoid this:

>>> Deck()
Deck(cards=[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), PlayingCard(rank='4', suit='♣'), PlayingCard(rank='5', suit='♣'), PlayingCard(rank='6', suit='♣'), PlayingCard(rank='7', suit='♣'), PlayingCard(rank='8', suit='♣'), PlayingCard(rank='9', suit='♣'), PlayingCard(rank='10', suit='♣'), PlayingCard(rank='J', suit='♣'), PlayingCard(rank='Q', suit='♣'), PlayingCard(rank='K', suit='♣'), PlayingCard(rank='A', suit='♣'), PlayingCard(rank='2', suit='♦'), PlayingCard(rank='3', suit='♦'), PlayingCard(rank='4', suit='♦'), PlayingCard(rank='5', suit='♦'), PlayingCard(rank='6', suit='♦'), PlayingCard(rank='7', suit='♦'), PlayingCard(rank='8', suit='♦'), PlayingCard(rank='9', suit='♦'), PlayingCard(rank='10', suit='♦'), PlayingCard(rank='J', suit='♦'), PlayingCard(rank='Q', suit='♦'), PlayingCard(rank='K', suit='♦'), PlayingCard(rank='A', suit='♦'), PlayingCard(rank='2', suit='♥'), PlayingCard(rank='3', suit='♥'), PlayingCard(rank='4', suit='♥'), PlayingCard(rank='5', suit='♥'), PlayingCard(rank='6', suit='♥'), PlayingCard(rank='7', suit='♥'), PlayingCard(rank='8', suit='♥'), PlayingCard(rank='9', suit='♥'), PlayingCard(rank='10', suit='♥'), PlayingCard(rank='J', suit='♥'), PlayingCard(rank='Q', suit='♥'), PlayingCard(rank='K', suit='♥'), PlayingCard(rank='A', suit='♥'), PlayingCard(rank='2', suit='♠'), PlayingCard(rank='3', suit='♠'), PlayingCard(rank='4', suit='♠'), PlayingCard(rank='5', suit='♠'), PlayingCard(rank='6', suit='♠'), PlayingCard(rank='7', suit='♠'), PlayingCard(rank='8', suit='♠'), PlayingCard(rank='9', suit='♠'), PlayingCard(rank='10', suit='♠'), PlayingCard(rank='J', suit='♠'), PlayingCard(rank='Q', suit='♠'), PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')])

I’ll note here that it’s not necessarily the extra keypresses that is the challenge. It’s that it breaks the flow of working interactively, when I need to always think about which object is returned and then treat different objects differently.

That said, I would probably not make the same kinds of trade-offs in a library package where it’s better to avoid surprises for users.

I’ve added a short paragraph in the original text tutorial emphasizing this point.

Become a Member to join the conversation.