The OOP Pillars Exam
A medium Python interview practice problem on DataDriven. Write the Python and run it against test cases, with instant feedback.
- Domain
- Python
- Difficulty
- medium
- Seniority
- L3
- Asked in
- Horizon Media
Problem
You're building the animal model for a small zoo, where a shared base animal defines that every species can speak and each species fills in its own sound. Given a list of [animal_type, name] pairs, build the matching animal for each and return the line it speaks, in the order the pairs arrive.
Four principles, one class hierarchy - show you know all of them.
Example
Input:
animals = [["dog","Rex"],["cat","Whiskers"],["dog","Buddy"]]
Output:
["Rex says: Woof!","Whiskers says: Meow!","Buddy says: Woof!"]
Worked solution and explanation
What this problem is really about
This is a polymorphic-dispatch drill wearing a zoo costume. The skill on trial: can you put each species' behavior INSIDE the type, so a base Animal declares a speak() contract and Dog and Cat each fill it in, and the driver just builds the right instance and asks it to speak? Anyone can write a dict from type string to sound. What actually separates candidates is refusing to branch on the type string in the driver: the pair NAMES a class, you construct it, and the instance answers. Get that wrong and either you call speak() on a raw list (which has no such method, so AttributeError), or your driver becomes the exact if animal_type == 'dog' ladder the hierarchy exists to delete.
Build it up
Step 1: Declare the base contract
Write Animal as a plain base class that stores the name and declares speak(). Have that base speak() raise NotImplementedError: it exists to name the contract every species must honor, not to do the work itself.
Step 2: Override in the subclasses
Create Dog and Cat that inherit from Animal and give their own speak() returning '{name} says: Woof!' and '{name} says: Meow!'. Each subclass owns exactly one behavior, so adding a species later is a new class, never an edit to an existing one.
Step 3: Encapsulate the name
Keep the name behind self._name in __init__. The underscore is the Python convention for 'internal state, read it through the object, not around it', so speak() reaches its own name without the caller poking at attributes.
Step 4: Dispatch through the instance
Map each type string to its class with a small registry dict, then for every pair look up the class, construct it with the name, and call speak() on the instance. The driver never inspects which species it built; the instance's own speak() runs. That is the polymorphism the problem is grading.
The solution
class Animal:
def __init__(self, name):
self._name = name
def speak(self):
raise NotImplementedError
class Dog(Animal):
def speak(self):
return f'{self._name} says: Woof!'
class Cat(Animal):
def speak(self):
return f'{self._name} says: Meow!'
def animal_sounds(animals):
registry = {'dog': Dog, 'cat': Cat}
result = []
for animal_type, name in animals:
cls = registry[animal_type]
instance = cls(name)
result.append(instance.speak())
return resultCommon follow-up questions
- What happens if you call speak() on a bare Animal instance, and how would you make instantiating Animal itself illegal? (Tests whether they understand the base contract is not callable work: base speak() raises NotImplementedError.)
- How would you add a new animal type without modifying the driver? (Tests the Open/Closed Principle.)
- What is the difference between a single underscore and a double underscore prefix in Python? (Tests name mangling vs convention-based privacy.)
- How does Python's duck typing relate to the polymorphism you used here? (Tests awareness that Python does not require inheritance for polymorphic behavior.)