Coding interleaved practice
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 1 — Tests 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.tallyreaches 3 after three constructions;aandchave no instancetally, so they read the class value3.b.tally = 99created an instance attribute onbonly — instance attrs shadow class attrs on lookup, butCounter.tallyis untouched.ids are 1, 2, 3 (assigned in construction order).
Problem 2 — Tests 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 = valueProblem 3 — Tests 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 4 — Tests 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 5 — Tests 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 6 — Tests 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 7 — Tests 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 8 — Tests 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.15from_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 9 — Tests 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 ** 2Problem 10 — Tests 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"}
]