Module 02: OOP Foundations & UML
Why this matters
"Design patterns" is really just a name for recurring, well-tested ways of combining four ideas: encapsulation, abstraction, inheritance, and polymorphism — the "four pillars" of object-oriented design. You already met encapsulation in module 01. This module completes the set, and adds the other half of what every LLD interview actually tests: can you draw your design before you code it? UML (Unified Modeling Language) is the shared notation for that — a class diagram is how you and an interviewer agree on a design's shape in five minutes instead of arguing over fifty lines of code.
Concepts
The four pillars, in one pass
| Pillar | One-line definition | Where you've already seen it |
|---|---|---|
| Encapsulation | Hide internal data behind methods/properties that control access | Module 01 — private fields, properties |
| Abstraction | Expose what something does, hide how | Below — abstract classes/interfaces |
| Inheritance | A class reuses and extends another class's shape | Below — base/derived classes |
| Polymorphism | Different classes respond to the same call in their own way | Below — overriding + calling through a common type |
Abstraction: interfaces and abstract classes
Abstraction means defining what operations exist without necessarily saying how they're implemented — a contract, not an implementation. Both languages let you declare a class that can't be instantiated directly, only used as a base that must be completed by a subclass.
from abc import ABC, abstractmethod
class Shape(ABC): # ABC = Abstract Base Class
@abstractmethod
def area(self) -> float:
... # no implementation — subclasses MUST provide one
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self) -> float:
return 3.14159 * self.radius ** 2
# shape = Shape() # TypeError: Can't instantiate abstract class Shape
circle = Circle(2)
print(circle.area()) # 12.56636
Python doesn't have a separate "interface" keyword — an ABC with only
@abstractmethods and no shared implementation is what other languages
call an interface; an ABC that also provides some real methods is what
other languages call an abstract class. Same mechanism, different intent.
Inheritance: reusing and extending a shape
Inheritance models an "is-a" relationship: a Dog is an
Animal. The derived (child) class automatically gets everything the
base (parent) class defines, and can add new members or override
existing ones.
class Animal:
def __init__(self, name):
self.name = name
def make_sound(self):
return "..."
class Dog(Animal): # Dog inherits from Animal
def make_sound(self): # OVERRIDING the base method
return "Woof!"
class Cat(Animal):
def make_sound(self):
return "Meow!"
d = Dog("Rex")
print(d.name) # "Rex" — inherited from Animal, not redefined in Dog
print(d.make_sound()) # "Woof!" — Dog's own version
A subclass constructor that needs the parent's setup calls it explicitly
with super().__init__(...):
class Puppy(Dog):
def __init__(self, name, age_months):
super().__init__(name) # runs Animal's __init__ first
self.age_months = age_months
Polymorphism: one call, many behaviors
Polymorphism ("many forms") means you can call the same method name
on a variable typed as the base/interface, and each actual object responds
in its own overridden way — without the calling code needing an if for
every possible type.
animals = [Dog("Rex"), Cat("Tom"), Animal("Generic")]
for animal in animals:
print(f"{animal.name}: {animal.make_sound()}")
# Rex: Woof!
# Tom: Meow!
# Generic: ...
The loop has no idea which concrete class each animal is — it just
calls .make_sound() and each object supplies its own behavior. This is
also why Python code rarely needs isinstance() checks: if every object
in a collection responds to .make_sound(), Python doesn't even
require they share a base class at all — this looser style is called
duck typing ("if it walks like a duck and quacks like a duck..."). An
ABC (as in the Abstraction section) is how you choose to enforce the
contract anyway, for safety and clarity.
UML class diagrams: drawing a design before coding it
A UML class diagram is a box per class, split into three sections:
┌─────────────────────────┐
│ Animal │ ← class name
├─────────────────────────┤
│ - name: string │ ← attributes (fields), with visibility
├─────────────────────────┤
│ + makeSound(): string │ ← methods, with visibility
└─────────────────────────┘
Visibility symbols prefix every attribute and method:
| Symbol | Means |
|---|---|
+ |
public |
- |
private |
# |
protected |
~ |
package/internal |
Relationship lines between boxes are where most of the actual design information lives:
Animal <|-- Dog inheritance/generalization (hollow triangle, solid line)
"Dog IS-A Animal"
IShape <|.. Circle realization/implementation (hollow triangle, DASHED line)
"Circle IMPLEMENTS IShape"
Car o-- Engine aggregation (hollow diamond)
"Car HAS-A Engine, but Engine can outlive the Car"
(e.g. swap the engine into a different car)
House *-- Room composition (filled diamond)
"House OWNS Room; a Room cannot outlive its House"
(destroy the House, its Rooms are destroyed with it)
Driver --> Car association (plain arrow)
"Driver USES/references a Car" — the weakest relationship
Order ..> PaymentGateway dependency (dashed arrow)
"Order calls PaymentGateway briefly" — not a stored reference,
just "this class depends on that one to do its job"
Aggregation vs. composition is the one distinction interviewers actually probe, because it changes your code's lifetime management: if deleting the container should also delete its parts, that's composition (the parts are typically created inside the container's constructor and never shared outside it); if the parts have independent existence, that's aggregation (they're typically passed in from outside).
Multiplicity (how many of one class relate to how many of another) is
written at each end of a line: Driver "1" --> "0..*" Car means one
Driver can be associated with zero or more Cars.
UML sequence diagrams: showing a flow over time
Where a class diagram shows structure (what exists), a sequence diagram shows behavior over time (what calls what, in order) — essential for explaining a use case like "what happens when a user places an order."
Customer Order PaymentGateway Inventory
| | | |
|--placeOrder()-->| | |
| |----charge()--------->| |
| | |--(success)-------->|
| |<---(charged)---------| |
| |------------reserveItems()--------------->|
| |<-----------------(reserved)---------------|
|<--(confirmed)---| | |
| | | |
Each vertical line is a lifeline (one object/participant). Each horizontal arrow is a message (a method call), read top to bottom in the order things actually happen. You'll draw diagrams exactly like this one, by hand, in module 11 (turning requirements into diagrams) and again in every classic-problem module from 12 onward — this is the actual whiteboard skill an LLD interview tests.
Hands-on exercises
1. Build the Animal hierarchy
Implement the Animal/Dog/Cat hierarchy from the Inheritance section
above, in both languages, plus one more subclass, Bird, that overrides
make_sound/MakeSound to return "Tweet!". Put all four in one
collection and loop over it printing each sound (the Polymorphism
example) — confirm you see all four distinct outputs from one loop with
one method call.
2. Add an abstract method
Make Animal/ShapeBase-style abstraction real: turn make_sound into
an abstract method (Python: @abstractmethod on an ABC; C#: abstract
on an abstract class) so that Animal itself can no longer be
instantiated directly, only its subclasses. Confirm attempting to
instantiate the base class now fails.
3. Draw it first
Before writing any code, draw (as text/ASCII, like the examples above) a
UML class diagram for a Library that has many Books (composition —
books are created by and belong to the library) and lends to many
Members (association — members exist independently of the library).
Include visibility symbols and at least one method on each class. Then
implement it in both languages and check your code actually matches what
you drew.
4. Sequence diagram for a real flow
Draw a sequence diagram (text/ASCII) for "a Member borrows a Book
from a Library" using the classes from exercise 3 — at minimum it
should show the Member calling something on Library, Library
checking the Book's availability, and a final confirmation back to
Member.
Independent challenge
No code given.
Task: Design and implement a small Shape hierarchy, in both
languages: an abstract Shape with abstract area() and perimeter()
methods, and at least three concrete subclasses (Circle, Rectangle,
Triangle). Then write a function that takes a collection of Shapes
(typed as the abstract base — this is the polymorphism part) and returns
the total area of all of them, without any if/isinstance/type
-checking inside that function. Draw the UML class diagram for this
hierarchy first, before writing the code, exactly as you practiced in
exercise 3.
Hint
The "no if/type-checking" constraint is the actual point of the
exercise: if you find yourself writing if isinstance(shape, Circle): ... elif isinstance(shape, Rectangle): ... inside the total-area
function, you've defeated the purpose of polymorphism — that logic
belongs inside each shape's own area() override, not in code that
consumes the shapes. The total-area function should be a single loop
calling .area() and summing, nothing else.
Common mistakes & troubleshooting
- Forgetting
virtual/overridein C#. If a base method isn't markedvirtual(orabstract), a subclass's same-named method doesn't override it — it silently hides it instead, and which version runs depends on the declared type of the variable, not the actual object, which is almost never what you want and is a genuine, common bug source. - Forgetting to call the base constructor. In C#, if the base class
has no parameterless constructor, you must call
: base(...)explicitly with matching arguments, or it won't compile. In Python, forgettingsuper().__init__(...)compiles fine but silently skips the parent's setup — a much sneakier bug because Python won't stop you. - Reaching for inheritance when you mean "has-a." Just because two classes share some behavior doesn't mean one should inherit from the other — inheritance should model "is-a," not "reuses some code from." (Module 05 covers this in depth: composition-over-inheritance.)
- Confusing interface and abstract class in C#. Interface: pure contract, no fields, a class can implement many. Abstract class: can mix real code with abstract requirements, a class can inherit from only one. If you need "shared code across several unrelated types," that's usually an interface each implements independently, not a forced shared abstract base.
- Drawing composition when you mean aggregation (or vice versa). Ask yourself: "if I delete the container, should the parts also die?" Yes → composition (filled diamond). No, they can be reused/reassigned elsewhere → aggregation (hollow diamond). Getting this wrong in an interview signals you haven't thought about object lifetime.
Checkpoint quiz
- Name the four pillars of OOP and give a one-sentence definition of each.
- In C#, what two keywords are both required to make method overriding work correctly, and where does each go?
- What's the practical difference between an interface and an abstract class in C#? Does Python distinguish them the same way?
- In the polymorphic "total area" example, why is it wrong for the
total-area function to contain
if isinstance(shape, Circle): ...? - What UML diamond (hollow or filled) represents composition, and what does that imply about object lifetime?
- What's the difference between association and dependency in UML?
Answers
- Encapsulation (hide data behind controlled access), abstraction (expose what, hide how), inheritance (a class reuses/extends another's shape, modeling "is-a"), polymorphism (the same call produces different behavior depending on the actual object).
virtualon the base class's method, andoverrideon the subclass's version. Both are required for real overriding to happen.- An interface is a pure contract (no implementation, no fields); a class can implement many. An abstract class can mix real implementation with abstract members; a class can inherit from only one. Python doesn't distinguish them with separate keywords — an ABC with only abstract methods plays the role of an interface, and one with some real methods plays the role of an abstract class.
- Because it defeats polymorphism's entire purpose — the "which
behavior for which type" decision should live inside each subclass's
own overridden method, not in the code that consumes them. The
if/isinstancechain also has to be updated every time a new shape is added, whereas the polymorphic version doesn't. - A filled diamond represents composition — it implies the "owned" object's lifetime is tied to its owner; destroying the container destroys its parts.
- Association is a stored reference/relationship one object holds onto (e.g., a field pointing at another object); dependency is a brief, non-stored "uses it to do a job" relationship (e.g., a parameter passed into one method call, never kept).
Interview questions
- "Explain the four pillars of OOP with a real example, not just
definitions."
Encapsulation: a
BankAccount's balance is private, only changed viadeposit/withdraw. Abstraction: callers ofShape.area()don't need to know how each shape computes it. Inheritance:SavingsAccountandCheckingAccountboth extend a sharedAccountbase. Polymorphism: a loop overList<Account>calling.calculateInterest()runs each account type's own logic without anifchain. - "What's the difference between method overriding and method overloading?" Overriding: a subclass replaces a base class's method with its own implementation (same name, same signature, runtime dispatch based on actual object type). Overloading: multiple methods with the same name but different parameter signatures in the same class, resolved at compile time based on the arguments passed — a different mechanism entirely (you saw this with C# constructor overloads in module 01).
- "When would you choose an abstract class over an interface, or vice
versa?"
Interface, when unrelated types need to promise the same capability
with no shared implementation (a
Flyableinterface implemented by bothBirdandAirplane, which share nothing else). Abstract class, when subclasses genuinely share a common "is-a" lineage and some real reusable implementation, not just a shared method signature. - "What is duck typing, and does C# support it?"
Duck typing (from Python's world): if an object has the method you're
about to call, you can call it, regardless of its declared type or
inheritance — "if it quacks like a duck." C# is statically typed and
requires a shared type (interface, base class, or
dynamic, rarely used) to call a method polymorphically — it doesn't support duck typing the way Python does by default. - "What's the difference between aggregation and composition, and why
does it matter in a design?"
Both are "has-a" relationships; the difference is lifetime
ownership. Composition: the part cannot exist independently of the
whole (a
Roominside aHouse) — when the whole is destroyed, so are its parts. Aggregation: the part has an independent lifetime and can be shared or reassigned (anEnginethat could be moved to a differentCar). It matters because it drives real decisions: who constructs the part, who's responsible for cleaning it up, and whether it can be shared.
Cumulative review
Closed-book. Pulls from modules 00–02.
- (00 + 01) What does
self/thisrefer to inside an instance method, and how is it made explicit differently in Python vs. C#? - (00 + 02)
7 / 3truncates to2in C#. Given that C# is statically typed, why does the compiler allow this instead of refusing to compile it? - (01 + 02) A property with a validating setter, and an abstract method, are both a form of the same OOP pillar. Which one, and why?
- (01 + 02) Why does forgetting
super().__init__()/: base(...)(module 01/02) tend to produce a much sneakier bug in Python than in C#? - (02) In the Animal/Dog/Cat polymorphism example, what would happen if
Dogdidn't overridemake_sound/MakeSoundat all?
Answers
- It refers to "the specific object this method was called on." Python
makes it explicit as the required first parameter (
self); C# makes it implicit (a bare field name already refers tothisobject's field), only writingthis.explicitly to disambiguate from a same-named parameter. - Because
int / intproducingint(with truncation) is itself a valid, well-typed operation in C#'s type system — there's no type error, just a numeric result you may not have wanted. The compiler only rejects code that doesn't type-check, not code that type-checks but produces a surprising value. - Abstraction. Both hide "how" behind a stable "what": the property's caller doesn't know or care that a check happens on write; the abstract method's caller doesn't know or care how each subclass actually computes the answer.
- C# requires an explicit
: base(...)call whenever the base class has no parameterless constructor, and refuses to compile if it's missing and required — the mistake is caught immediately. Python'ssuper().__init__(...)is just a normal method call you can forget entirely; the code still runs, just with the parent's setup silently skipped, so the bug surfaces later as missing/uninitialized data instead of at write-time. - It would inherit
Animal's ownmake_sound/MakeSoundunchanged and printAnimal's generic sound ("...") instead of "Woof!" — perfectly legal, just not what you intended; this is exactly why the pattern is called "overriding" and not "required per subclass."
Further reading & sources
- Python:
abc— Abstract Base Classes - official reference forABC/@abstractmethod. - Microsoft Learn: Inheritance (C#) -
virtual/override/basein depth. - Microsoft Learn: Interfaces (C#) - official interface reference, including multiple-implementation.
- UML class diagram relationships (association, aggregation, composition, generalization) - the authoritative visual notation reference this module's ASCII diagrams are based on.
- UML sequence diagrams reference - lifelines, messages, activation bars in full notation.
Next
03-generics-exceptions-and-value-objects — generic/type-safe classes and methods, designing an exception hierarchy instead of throwing generic errors everywhere, and modeling immutable value objects with enums and records/dataclasses.