Interleaved — Phase 2

Coding interleaved practice

printable — key stays hidden on paper

Instructions: Solve each problem independently. These problems mix class mechanics, inheritance, dunder methods, and design principles in deliberately random order — read carefully and decide which concept applies before answering. Write code where asked; predict output where asked; name the principle where asked. Total: 50 marks.


Problem 1 (5 marks) Predict the exact printed output:

class Counter:
    tally = 0
    def __init__(self):
        Counter.tally += 1
        self.id = Counter.tally
 
a = Counter(); b = Counter(); c = Counter()
b.tally = 99
print(a.tally, b.tally, c.tally, Counter.tally)
print(a.id, b.id, c.id)

Explain in one line why b.tally differs from a.tally.


Problem 2 (6 marks) A Rectangle class exposes width and height as public attributes. A client wants to guarantee that width can never be set to a negative number, without breaking existing code that does r.width = 5. Write the class using the appropriate feature so r.width = -3 raises ValueError.


Problem 3 (4 marks) Consider this design smell:

class Report:
    def generate(self): ...
    def save_to_disk(self): ...
    def send_email(self): ...
    def format_as_pdf(self): ...

Name the SOLID principle violated and describe the refactor in 2 sentences.


Problem 4 (6 marks) Given the diamond hierarchy, write the MRO (order of classes Python searches) for D, and predict what D().greet() prints:

class A:
    def greet(self): print("A"); 
class B(A):
    def greet(self): print("B"); super().greet()
class C(A):
    def greet(self): print("C"); super().greet()
class D(B, C):
    def greet(self): print("D"); super().greet()
 
D().greet()

Problem 5 (5 marks) Implement a Money class supporting Money(5) + Money(3) == Money(8) and usable as a dict key. Which dunder methods must you define and why must __eq__ and __hash__ be defined together?


Problem 6 (4 marks) A junior dev writes a Bird base class with a fly() method, then makes Penguin(Bird) override fly() to raise NotImplementedError. Name the principle this breaks and give the correct redesign in one sentence.


Problem 7 (5 marks) Predict the output and explain the self mechanics:

class Greeter:
    def hi(self): return f"hi from {id(self)}"
 
g = Greeter()
print(g.hi() == Greeter.hi(g))

Why are these two calls equivalent?


Problem 8 (6 marks) Write a class Temperature where the classmethod from_fahrenheit(f) is an alternative constructor and a staticmethod is_valid(celsius) returns whether a value is above absolute zero (−273.15). Explain why one is @classmethod and the other @staticmethod.


Problem 9 (5 marks) Given:

from abc import ABC, abstractmethod
class Shape(ABC):
    @abstractmethod
    def area(self): ...
 
s = Shape()

What happens on the last line and why? Then write a valid concrete subclass Circle.


Problem 10 (4 marks) Two designs for a Car:

  • (A) class Car(Engine): ...
  • (B) class Car: def __init__(self): self.engine = Engine()

Which models "has-a" and which "is-a"? Which is correct for Car/Engine and why?


Answer keyMark scheme & solutions

Problem 1Tests 2.1.3 (instance vs class attributes) + 2.1.5. Why this method: the trap is b.tally = 99 creates an instance attribute shadowing the class attribute, so you must distinguish lookup order.

Output:

3 99 3 3
1 2 3
  • Counter.tally reaches 3 after three constructions; a and c have no instance tally, so they read the class value 3.
  • b.tally = 99 created an instance attribute on b only — instance attrs shadow class attrs on lookup, but Counter.tally is untouched.
  • ids are 1, 2, 3 (assigned in construction order).

Problem 2Tests 2.1.7 (properties). Why this method: need validation without changing the r.width = 5 interface → @property + @setter is exactly the tool (not getters/setters).

class Rectangle:
    def __init__(self, width, height):
        self.width = width      # goes through setter
        self.height = height
 
    @property
    def width(self):
        return self._width
 
    @width.setter
    def width(self, value):
        if value < 0:
            raise ValueError("width must be non-negative")
        self._width = value
 
    @property
    def height(self):
        return self._height
 
    @height.setter
    def height(self, value):
        if value < 0:
            raise ValueError("height must be non-negative")
        self._height = value

Problem 3Tests 2.2.4 (Single Responsibility). Why: four unrelated responsibilities (generation, persistence, email, formatting) in one class → SRP.

Violates the Single Responsibility Principle. Split into separate classes/objects: Report (generation only), ReportSaver, EmailSender, PdfFormatter. Each should have exactly one reason to change, so a change to email logic never risks breaking PDF formatting.


Problem 4Tests 2.1.10 (C3 linearization) + 2.1.9 (super). Why: cooperative super() follows the MRO, not the literal parent — must compute C3 order.

MRO of D: D → B → C → A → object.

Output:

D
B
C
A

Each super().greet() advances to the next class in D's MRO — so B's super goes to C (not A), which is the whole point of cooperative multiple inheritance.


Problem 5Tests 2.1.14 (operator overloading / dunders). Why: value-equality + arithmetic + hashability all require dunders; interleaved with the hash/eq contract.

class Money:
    def __init__(self, amount):
        self.amount = amount
    def __add__(self, other):
        return Money(self.amount + other.amount)
    def __eq__(self, other):
        return isinstance(other, Money) and self.amount == other.amount
    def __hash__(self):
        return hash(self.amount)
    def __repr__(self):
        return f"Money({self.amount})"

__eq__ and __hash__ must agree: defining __eq__ alone makes the class unhashable (Python sets __hash__=None), and objects that compare equal must hash equal — otherwise dict/set lookups break.


Problem 6Tests 2.2.6 (Liskov Substitution). Why: subclass can't be substituted for its base without breaking behavior → LSP.

Breaks the Liskov Substitution Principle (a Penguin can't stand in for a Bird that flies). Redesign: give Bird no fly(); add a separate FlyingBird subclass (or a Flyer capability interface) and make Penguin inherit only the abilities it actually has.


Problem 7Tests 2.1.5 (self mechanics). Why: the equivalence reveals that self is just the first positional argument bound by the descriptor protocol.

Output: True. g.hi() is syntactic sugar: Python looks up hi on the class, and the method descriptor binds g as the first argument. So g.hi() becomes exactly Greeter.hi(g) — same self, same id, same string.


Problem 8Tests 2.1.4 (classmethod vs staticmethod). Why: alternative constructor needs the class → @classmethod; pure validation logic needs neither instance nor class → @staticmethod.

class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius
 
    @classmethod
    def from_fahrenheit(cls, f):
        return cls((f - 32) * 5 / 9)
 
    @staticmethod
    def is_valid(celsius):
        return celsius >= -273.15

from_fahrenheit is a @classmethod because it constructs and returns an instance via cls (works correctly under subclassing). is_valid is @staticmethod because it's a pure function grouped with the class but needing no self/cls.


Problem 9Tests 2.1.13 (ABC / abstractmethod). Why: instantiating an ABC with an unimplemented abstract method is the defining behavior.

s = Shape() raises TypeError: Can't instantiate abstract class Shape with abstract method area. ABCs with unimplemented @abstractmethods cannot be instantiated.

import math
class Circle(Shape):
    def __init__(self, r):
        self.r = r
    def area(self):
        return math.pi * self.r ** 2

Problem 10Tests 2.1.15 (composition vs inheritance). Why: distinguishing has-a from is-a is the core decision.

(A) is inheritance = "is-a" (Car is an Engine — wrong). (B) is composition = "has-a" (Car has an Engine — correct). A car is not a kind of engine; it contains one, so composition (B) is right.

[
  {"claim":"Problem 1: Counter.tally reaches 3 after three instances",
   "code":"class Counter:\n    tally=0\n    def __init__(self):\n        Counter.tally+=1\n        self.id=Counter.tally\na=Counter();b=Counter();c=Counter()\nb.tally=99\nresult = (a.tally==3 and b.tally==99 and c.tally==3 and Counter.tally==3 and (a.id,b.id,c.id)==(1,2,3))"},
  {"claim":"Problem 4: MRO of D is D,B,C,A,object",
   "code":"class A:\n    def greet(self): pass\nclass B(A):\n    def greet(self): pass\nclass C(A):\n    def greet(self): pass\nclass D(B,C):\n    def greet(self): pass\nresult = [cls.__name__ for cls in D.__mro__]==['D','B','C','A','object']"},
  {"claim":"Problem 5: equal Money objects hash equal and add correctly",
   "code":"class Money:\n    def __init__(self,a): self.amount=a\n    def __add__(self,o): return Money(self.amount+o.amount)\n    def __eq__(self,o): return isinstance(o,Money) and self.amount==o.amount\n    def __hash__(self): return hash(self.amount)\nr = (Money(5)+Money(3))==Money(8) and hash(Money(8))==hash(Money(8)) and len({Money(2),Money(2)})==1\nresult = r"}
]