0%

Dataclasses and Value Objects · practice

Class Attributes and Instance Attributes

Every attribute you have written so far was assigned in __init__ or listed in a dataclass, and each got its own. There is a second place an attribute can live, and it belongs to the class rather than to any object.

One value for the whole class

Try it

default_points is assigned in the class body, not in __init__. There is exactly one of it, shared by every Question, and it can be read through the class itself without any object existing.

Instance attributes answer “what is true of this one object?” Class attributes answer “what is true of every object of this kind?”

Reading, and the surprise

Reading works through either. Assigning does not do what it looks like:

Try it

5, 1, 1.

Assigning through an object did not change the class attribute. It created a new instance attribute on first that shadows it. second and the class are untouched, and first now has its own that will never follow the class again.

To change it for everyone, through the class:

Try it

first still reports 5, because it has its own now. second reports 3, because it is still reading the class.

A class attribute is shared by instances until one instance shadows it with its own value.

After first.default_points = 5, why does second.default_points still report the old value?

The one that bites

That rule is mostly a curiosity. It becomes a bug when the class attribute is :

Try it

The final exam contains the review’s question. self.questions.append(...) never assigned anything, so no instance attribute was created: it found the one shared and appended to it.

This is the same trap as the mutable default from Lesson 4, arriving by a different door. The fix is the same idea: give each object its own.

class Quiz:
    def __init__(self, title):
        self.title = title
        self.questions = []

In a dataclass, use field(default_factory=list). A mutable class attribute is shared state. Use one only when that sharing is deliberate and the class owns clear operations for it; per-object collections belong on each instance.

What class attributes are actually good for

Used for their real purpose, they are useful and safe. Constants that belong to the class:

Try it

MAX_POINTS is a fact about questions in general, so keeping it in the class is more honest than a -level , and it is readable as Question.MAX_POINTS from anywhere. The uppercase name is the usual signal that a value is not meant to change.

Note what @dataclass did with those two lines: nothing. They have no annotation, so they are not fields, they do not appear in __init__ or the repr, and they are not compared. For the syntax used in this chapter, an annotated name is a field and an unannotated name is a plain class attribute. Advanced annotations such as typing.ClassVar can explicitly mark an annotated class attribute, but that is outside this lesson.

Try it

No MAX_POINTS in the repr, exactly as intended.

The exercise uses class attributes for the two things they are good at, and repairs one that was used for something they are bad at.

Task

Use class attributes for what they are good at, and stop using them for what they are not.

Question should carry MIN_POINTS = 1 and MAX_POINTS = 10 as class attributes, with no annotation, so they stay out of the generated __init__, repr, and equality. Use them in __post_init__ to reject points outside the inclusive . Keep prompt and answer non-empty, too.

Quiz has a bug: _questions is a class attribute, so every quiz shares one . Give each quiz its own internal _questions list, refuse None in add, and return a copy from questions().

QuizAttempt should count how many attempts have been created, using a class attribute count that __init__ increments through the class. Every attempt should agree about the total, since it is a fact about attempts in general rather than about any one of them.

Run the program. The final exam must not contain the review’s questions, an out-of-range question must be refused, and both attempts must report the same count.