Quiz: Inheritance and Polymorphism
Test your understanding before moving on. Target: 70% or higher to proceed confidently.
Section 1: Multiple Choice (1 point each)
1. What does the syntax class Dog(Animal): mean?
- A)
Dogcontains an instance ofAnimal(composition) - B)
Doginherits fromAnimal(inheritance) - C)
DogandAnimalare the same class - D)
Animalinherits fromDog
Answer
**B)** `Dog` inherits from `Animal`. *Why B:* The parentheses after the class name specify the parent class. `Dog` gets all of `Animal`'s methods and attributes. *Why not A:* Composition is when a class has an instance of another class as an attribute, e.g., `self.animal = Animal()`. *Why not C/D:* Inheritance has a direction — the class in parentheses is the parent. *Reference:* Section 15.22. What is method overriding?
- A) Defining a new method that doesn't exist in the parent class
- B) Defining a method in a subclass with the same name as one in the parent, replacing the parent's version
- C) Calling
super()to run the parent's method - D) Preventing a method from being changed by subclasses
Answer
**B)** Defining a method in a subclass with the same name as one in the parent, replacing the parent's version. *Why B:* Overriding means the subclass provides its own implementation that takes precedence over the parent's. *Why not A:* Adding a new method is just adding a method — it's not overriding anything. *Why not C:* Calling `super()` is a technique used *within* an override to extend behavior, but it's not what overriding means. *Why not D:* Python doesn't have a built-in mechanism to prevent overriding (unlike Java's `final`). *Reference:* Section 15.33. What happens if a subclass defines __init__ but does NOT call super().__init__()?
- A) Python automatically calls the parent's
__init__ - B) The parent's attributes are never initialized, likely causing
AttributeErrorlater - C) A
SyntaxErroris raised - D) The subclass cannot be instantiated
Answer
**B)** The parent's attributes are never initialized, likely causing `AttributeError` later. *Why B:* If the subclass overrides `__init__` without calling `super().__init__()`, the parent's `__init__` never runs, so attributes it would have set don't exist. *Why not A:* Python does NOT automatically call the parent's `__init__` if the subclass defines its own. *Why not C:* There's no syntax error — it's legal code. The bug appears at runtime. *Why not D:* You can create the instance, but accessing parent attributes will fail later. *Reference:* Section 15.4 (Debugging Walkthrough)4. Which of the following BEST describes polymorphism?
- A) A class that inherits from multiple parents
- B) Different objects responding to the same method call in their own way
- C) Hiding implementation details behind a simple interface
- D) Using
isinstance()to check an object's type before calling methods
Answer
**B)** Different objects responding to the same method call in their own way. *Why B:* Polymorphism means "many forms" — the same method name triggers different behavior depending on the object. *Why not A:* That's multiple inheritance, a different concept. *Why not C:* That's closer to encapsulation or abstraction. *Why not D:* Using `isinstance()` is actually the *opposite* of polymorphism — it's manual type-checking that polymorphism eliminates. *Reference:* Section 15.55. What does the @abstractmethod decorator do?
- A) Makes a method private so subclasses cannot access it
- B) Makes a method static so it doesn't require
self - C) Requires subclasses to implement the method; prevents instantiation of the abstract class
- D) Automatically generates the method body
Answer
**C)** Requires subclasses to implement the method; prevents instantiation of the abstract class. *Why C:* `@abstractmethod` marks a method as "must be implemented by subclasses." Any class with unimplemented abstract methods cannot be instantiated. *Why not A:* Python doesn't have a `private` keyword. Abstract methods are meant to be overridden, not hidden. *Why not B:* That's `@staticmethod`. *Why not D:* The abstract method has no auto-generated body; the subclass must provide one. *Reference:* Section 15.66. When should you use composition instead of inheritance?
- A) When the relationship is "has-a" (a Car has an Engine)
- B) When the relationship is "is-a" (a Dog is an Animal)
- C) When you want to save typing by inheriting methods
- D) Whenever you want to reuse code from another class
Answer
**A)** When the relationship is "has-a" (a Car has an Engine). *Why A:* Composition models containment — one object contains another. "Has-a" relationships should use composition. *Why not B:* "is-a" relationships should use inheritance. *Why not C:* "Saving typing" is not a reason to use inheritance. If the relationship isn't truly "is-a," use composition. *Why not D:* Code reuse can be achieved through both inheritance and composition. The choice depends on the relationship. *Reference:* Section 15.77. What is the Method Resolution Order (MRO)?
- A) The order in which methods are defined within a class
- B) The order in which Python searches classes in a hierarchy when looking up a method
- C) The order in which
__init__methods are called - D) The alphabetical ordering of method names
Answer
**B)** The order in which Python searches classes in a hierarchy when looking up a method. *Why B:* When you call a method on an object, Python checks the object's class first, then its parent, then the parent's parent, etc. The MRO defines this search order, which matters especially with multiple inheritance. *Why not A:* The order methods are defined in the source code doesn't affect lookup. *Why not C:* MRO applies to all method lookups, not just `__init__`. *Why not D:* MRO has nothing to do with alphabetical order. *Reference:* Section 15.88. What is the output of this code?
class Parent:
def speak(self):
return "Parent speaks"
class Child(Parent):
def speak(self):
return "Child speaks"
obj = Child()
print(obj.speak())
- A)
Parent speaks - B)
Child speaks - C)
Parent speaksfollowed byChild speaks - D)
AttributeError
Answer
**B)** `Child speaks` *Why B:* `Child` overrides `speak()`, so the subclass version is called. *Why not A:* The parent's version is shadowed by the override. *Why not C:* Only the child's version runs. `super().speak()` is not called. *Why not D:* The method exists in `Child`, so no error occurs. *Reference:* Section 15.39. What does isinstance(obj, ParentClass) return when obj is an instance of a child class?
- A)
True - B)
False - C) It raises a
TypeError - D) It depends on the child class
Answer
**A)** `True` *Why A:* An instance of a child class is also considered an instance of the parent class due to the "is-a" relationship. *Why not B:* `isinstance` checks the entire class hierarchy, not just the exact class. *Why not C:* This is valid usage of `isinstance`. *Why not D:* It's always `True` for any child class in the hierarchy. *Reference:* Section 15.210. Which of the following violates the Liskov Substitution Principle?
- A) A
Dogsubclass that overridesspeak()to return"Woof"instead of the parentAnimal's"...". - B) A
Penguinsubclass ofBirdthat raisesNotImplementedErrorfromfly(). - C) A
Squaresubclass ofShapethat implementsarea()differently fromRectangle. - D) A
SortedListsubclass oflistthat overridesappend()to maintain sort order.
Answer
**B)** A `Penguin` subclass of `Bird` that raises `NotImplementedError` from `fly()`. *Why B:* If code expects all `Bird` objects to be able to `fly()`, a `Penguin` that throws an error violates the expectation that subclasses can be used wherever the parent is used. *Why not A:* Overriding `speak()` with a different return value is the whole point of polymorphism. *Why not C:* Different shapes calculating area differently is expected and correct. *Why not D:* While this changes `append()` behavior, it still adds the item (just in sorted position), so it doesn't break expectations in a harmful way. *Reference:* Section 15.9Section 2: Short Answer (2 points each)
11. Write a minimal class hierarchy: an Animal base class with a sound() method, and Dog and Cat subclasses that override sound(). Show that a single loop can call sound() on all three types.
Answer
class Animal:
def sound(self) -> str:
return "..."
class Dog(Animal):
def sound(self) -> str:
return "Woof"
class Cat(Animal):
def sound(self) -> str:
return "Meow"
animals = [Dog(), Cat(), Animal()]
for a in animals:
print(a.sound())
# Woof
# Meow
# ...
The key point is that the loop doesn't check types — it calls `sound()` on each object and polymorphism ensures the right version runs.
*Reference:* Section 15.5
12. Explain the difference between overriding a method and extending it with super(). Give a one-line example of each.
Answer
**Overriding** completely replaces the parent's method:def describe(self):
return f"Dog: {self.name}" # Parent's describe() is not called
**Extending** calls the parent's method and adds to it:
def describe(self):
return super().describe() + f" (breed: {self.breed})"
Overriding is "replacing." Extending is "doing what the parent does, plus more."
*Reference:* Section 15.4
13. Why can't you create an instance of an abstract base class? What error do you get if you try?
Answer
An abstract base class has at least one `@abstractmethod` — a method with no complete implementation. Python prevents instantiation because the object would have "missing" behavior. The error is:TypeError: Can't instantiate abstract class ClassName with abstract method method_name
The abstract class exists to define a contract (what methods subclasses must implement), not to be used directly. Only concrete subclasses that implement all abstract methods can be instantiated.
*Reference:* Section 15.6
14. A Stack data structure supports push(), pop(), and peek(). Should Stack inherit from Python's list? Why or why not?
Answer
**No.** A `Stack` is not a `list` — it "has-a" list internally, but it only exposes a restricted interface (`push`, `pop`, `peek`). If `Stack` inherited from `list`, users could call `insert()`, `sort()`, `reverse()`, and other `list` methods that violate the stack's LIFO contract. This is a "has-a" relationship (the stack *uses* a list), so composition is correct:class Stack:
def __init__(self):
self._items = [] # Composition: Stack HAS a list
def push(self, item):
self._items.append(item)
def pop(self):
return self._items.pop()
*Reference:* Section 15.7
15. What is the MRO of this class? Predict, then verify with print(D.__mro__).
class A:
pass
class B(A):
pass
class C(A):
pass
class D(B, C):
pass
Answer
D -> B -> C -> A -> object
Python uses C3 linearization to compute the MRO. It follows a depth-first, left-to-right order while respecting the constraint that a class always appears before its parents. Since both `B` and `C` inherit from `A`, `A` appears only once, after both `B` and `C`.
print(D.__mro__)
# (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
*Reference:* Section 15.8
Section 3: Code Analysis (3 points each)
16. What is the output of this code? Trace through the execution step by step.
class Base:
def __init__(self, x):
self.x = x
print(f"Base.__init__: x={self.x}")
def show(self):
print(f"Base.show: x={self.x}")
class Middle(Base):
def __init__(self, x, y):
super().__init__(x)
self.y = y
print(f"Middle.__init__: y={self.y}")
class Top(Middle):
def __init__(self, x, y, z):
super().__init__(x, y)
self.z = z
print(f"Top.__init__: z={self.z}")
def show(self):
super().show()
print(f"Top.show: y={self.y}, z={self.z}")
t = Top(1, 2, 3)
t.show()
Answer
Base.__init__: x=1
Middle.__init__: y=2
Top.__init__: z=3
Base.show: x=1
Top.show: y=2, z=3
**Trace:**
1. `Top.__init__(1, 2, 3)` calls `super().__init__(1, 2)` which calls `Middle.__init__`.
2. `Middle.__init__(1, 2)` calls `super().__init__(1)` which calls `Base.__init__`.
3. `Base.__init__` sets `self.x = 1` and prints.
4. Control returns to `Middle.__init__`, which sets `self.y = 2` and prints.
5. Control returns to `Top.__init__`, which sets `self.z = 3` and prints.
6. `t.show()` calls `Top.show()`, which first calls `super().show()`.
7. `super().show()` looks up the MRO: `Top -> Middle -> Base`. `Middle` doesn't define `show()`, so `Base.show()` runs and prints `x=1`.
8. Control returns to `Top.show()`, which prints `y=2, z=3`.
*Reference:* Sections 15.2, 15.4
17. This code has a design flaw. Identify it and explain how to fix it.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
...
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return 3.14159 * self.radius ** 2
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
# Someone tries to use Rectangle:
r = Rectangle(5, 10)
print(r.area())
Answer
`Rectangle` does not implement the abstract `area()` method. When you try to create `Rectangle(5, 10)`, Python raises:TypeError: Can't instantiate abstract class Rectangle with abstract method area
**Fix:** Add the `area()` method to `Rectangle`:
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
This is exactly the kind of bug that abstract base classes are designed to catch — at instantiation time rather than when `area()` is called.
*Reference:* Section 15.6
18. Analyze this code. Is the inheritance relationship appropriate? Why or why not?
class List:
def __init__(self):
self.items = []
def add(self, item):
self.items.append(item)
def get(self, index):
return self.items[index]
def size(self):
return len(self.items)
class Queue(List):
def enqueue(self, item):
self.add(item)
def dequeue(self):
return self.items.pop(0)
Answer
The inheritance relationship is **problematic**. A `Queue` only allows adding to the back and removing from the front, but inheriting from `List` exposes `get(index)` which allows direct access to any element, violating the queue's FIFO contract. A user could write `queue.get(3)` to access the fourth element, which queues shouldn't allow. This is a "has-a" relationship — a queue *uses* a list internally. Better design:class Queue:
def __init__(self):
self._items = [] # Composition
def enqueue(self, item):
self._items.append(item)
def dequeue(self):
if not self._items:
raise IndexError("Queue is empty")
return self._items.pop(0)
def size(self):
return len(self._items)
This hides the internal list and only exposes queue-appropriate operations.
*Reference:* Section 15.7
Section 4: Design Questions (3 points each)
19. You're designing a document processing system with PDFDocument, WordDocument, and SpreadsheetDocument. Each can be opened, saved, and exported_to_text. Design the class hierarchy using an abstract base class. Write the class signatures (with __init__ and method signatures, but you can use ... for method bodies).
Answer
from abc import ABC, abstractmethod
class Document(ABC):
def __init__(self, filename: str):
self.filename = filename
self.content = ""
@abstractmethod
def open(self) -> None:
...
@abstractmethod
def save(self) -> None:
...
@abstractmethod
def export_to_text(self) -> str:
...
def __str__(self) -> str:
return f"{type(self).__name__}: {self.filename}"
class PDFDocument(Document):
def open(self) -> None: ...
def save(self) -> None: ...
def export_to_text(self) -> str: ...
class WordDocument(Document):
def open(self) -> None: ...
def save(self) -> None: ...
def export_to_text(self) -> str: ...
class SpreadsheetDocument(Document):
def open(self) -> None: ...
def save(self) -> None: ...
def export_to_text(self) -> str: ...
The abstract base class ensures all document types implement the required methods. Shared behavior (like `__str__`) is in the parent.
*Reference:* Section 15.6
20. Compare these two designs for a music player. Which is better and why?
Design A (Inheritance):
class MusicPlayer(Playlist, AudioDecoder, Equalizer, Visualizer):
pass
Design B (Composition):
class MusicPlayer:
def __init__(self):
self.playlist = Playlist()
self.decoder = AudioDecoder()
self.equalizer = Equalizer()
self.visualizer = Visualizer()