Inheritance and Polymorphism · practice
Replace an Unhelpful Hierarchy
Inheritance is easy to reach for and hard to back out of. This lesson is about noticing that you have reached too far, and about what to do next.
A hierarchy that grew
Here is a family that started sensibly and kept going:
class Question:
...
class GradedQuestion(Question):
...
class TimedGradedQuestion(GradedQuestion):
...
class TimedGradedShuffledQuestion(TimedGradedQuestion):
...
Each class was added for a real reason. Together they describe a problem the hierarchy cannot solve: what happens when someone wants a timed question that is not graded, or a shuffled question that is not timed?
Every combination of independent features needs its own class, and the number of classes doubles with each new feature. Timed and shuffled and graded are not three kinds of question. They are three things a question might separately be.
The signals
You have gone too far when you see:
Names that are TimedGradedShuffledQuestion is a class name that admits it is a combination.
A subclass that ignores most of what it inherits. If eight of ten inherited
Overrides that raise or do nothing. Lesson 8’s LockedQuestion refusing reword is a subclass failing to be its base.
Depth for its own sake. Each extra level needs to justify the behavior it shares. Every level is another place a method might be hiding, so deep hierarchies deserve more scrutiny than shallow ones.
A base that keeps growing. If adding a subclass means adding an attribute to the base that only that subclass uses, the base has become a union of its children.
Which of these most strongly suggests a hierarchy has gone wrong?
Replacing it with composition
The way out is the answer Chapter 6 gave: what a question is stays inheritance, and what a question has becomes composition.

One class of question, with or without a time limit. Add shuffling and it is another optional collaborator, not a doubling of the class list. The combinations are made at construction time by the caller, rather than enumerated in advance by you.
What to keep as inheritance
Notice that TextQuestion(Question) survived. It should.
The difference is that being a text question is a genuine kind: it changes what is_correct means, and no question is a text question and a numeric question at once. Timing is not a kind. A question either has a time limit or does not, and the same question could have a different one tomorrow.
The working question: is this a kind of thing, or a thing it has? Kinds that are mutually exclusive and change the
Changing it after the fact
Untangling a hierarchy in working code is a refactoring, and it goes in small steps:
pick one feature that is not a kind, such as timing;
write the small class that owns it;
give the base an optional attribute for it, defaulting to nothing;
move that feature’s behavior out of the subclasses into the new class;
delete any subclass that existed only to add it;
keep the tests passing at every step.
The last one matters most, and it is the whole subject of Chapter 13. Refactoring without tests is rewriting and hoping.
Not an argument against inheritance
The point is not that inheritance is bad. It is one tool, and this chapter has used it well: a small family of question
The failure mode is treating it as the way to share anything at all. Composition combines freely, inheritance does not, so composition is the default, and inheritance is what you reach for when the “is a” is true.
Task
Six classes here describe two kinds of question and two optional features. Reduce them to three classes and two collaborators.
Keep TextQuestion and NumericQuestion as subclasses of Question. Being a text question genuinely changes what is_correct means, and no question is both.
Turn timing and hints into things a question has. Write a validated TimeLimit(seconds) requiring positive seconds and a validated Hint(text) requiring non-empty text. Give Question optional timing and hint attributes, both defaulting to None, while retaining non-empty prompt and non-negative points validation.
Question.describe() returns "<prompt> (<points> points)", followed by a space and each collaborator’s description that is present, timing before hint.
Keep Lesson 8’s response contract: numeric questions return False for blank or nonnumeric text.
Delete TimedTextQuestion, HintedTextQuestion, and TimedHintedTextQuestion. Any combination should now be built by passing collaborators, including combinations nobody wrote a class for.