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
One value for the whole class
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:
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
To change it for everyone,
first still reports 5, because it has its own now. second reports 3, because it is still reading the class.

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
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
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:
MAX_POINTS is a fact about questions in general, so keeping it in the class is more honest than a 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.
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
Quiz has a bug: _questions is a _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.