Vasu Patel logoVasu Patel
← Objects First: OOP Fundamentals
All chapters in this track
  1. 01Chapter 1: Why Object Orientation Exists
  2. 02Chapter 2: Classes, Objects, and Instances, Precisely Defined
  3. 03Chapter 3: The Four Pillars I: Encapsulation
  4. 04Chapter 4: The Four Pillars II: Abstraction
  5. 05Chapter 5: The Four Pillars III: Inheritance
  6. 06Chapter 6: The Four Pillars IV: Polymorphism
  7. 07Chapter 7: Composition vs. Inheritance
  8. 08Chapter 8: Interfaces and Abstract Classes: When to Use Which
  9. 09Chapter 9: SOLID, Part 1: Single Responsibility & Open/Closed
  10. 10Chapter 10: SOLID, Part 2: Liskov Substitution, Interface Segregation, Dependency Inversion
  11. 11Chapter 11: Basic UML for Communicating Design
  12. 12Chapter 12: Anti-Patterns: God Objects, Tight Coupling, Deep Hierarchies, the Fragile Base Class, Anemic Domain Models
  13. 13Chapter 13: OOP Idioms Across TypeScript and Python — and Where OOP Is the Wrong Tool
  14. 14Chapter 14: When OOP Breaks Down: Real-World Failure Modes
  15. 15Chapter 15: Design Exercise: Parking Lot System
  16. 16Chapter 16: Design Exercise: Library Management System
  17. 17Chapter 17: Design Exercise: Elevator Controller
  18. 18Chapter 18: OOP in Interviews: What a Strong Answer Looks Like

Chapter 13 of 18

Chapter 13: OOP Idioms Across TypeScript and Python — and Where OOP Is the Wrong Tool

Why this matters

Every example so far has shown the same concept in both TypeScript and Python, and by now a pattern should be visible: they're conceptually identical but idiomatically different. Knowing where they diverge — and knowing when neither language's OOP toolkit is actually the right answer — is what separates someone who's memorized syntax in two languages from someone who understands OOP as a set of trade-offs.

Where TypeScript and Python's OOP diverge

[IN PRACTICE]

ConceptTypeScriptPython
Access controlCompile-time only (private/protected); #field for real runtime privacyConvention only (_field, __field name-mangling)
Interfacesinterface keyword, structural typing (a type satisfies an interface by shape, checked at compile time)Protocol (structural, PEP 544) or ABC (nominal, explicit inheritance)
Multiple inheritanceNot supported for classes (single inheritance + multiple interfaces)Supported directly (class C(A, B)), resolved via the C3 linearization / MRO (Method Resolution Order)
Method overloadingSupported via multiple type signatures for one implementationNot supported natively — conventionally handled with default arguments, *args/**kwargs, or functools.singledispatch
"Duck typing"Structural typing is compile-time enforced (still statically checked)True runtime duck typing — if an object has the right methods, it works, checked only when called
Data classesPlain classes, or libraries; no built-in field-declaration shorthand (though interfaces + object literals cover many cases)@dataclass auto-generates __init__, __eq__, __repr__ from field declarations
Operator overloadingNot supported (no + overloading on custom classes)Supported via dunder methods (__add__, __eq__, __lt__, etc.)

Multiple inheritance and the MRO

[SENIOR INSIGHT] Python's support for multiple inheritance is a genuine capability TypeScript lacks entirely — and it comes with genuine complexity most engineers only learn about after being surprised by it: Method Resolution Order (MRO).

class Notifier:
    def notify(self) -> str:
        return "Notifier"

class Loggable:
    def notify(self) -> str:
        return "Loggable"

class Service(Notifier, Loggable):
    pass

print(Service().notify())        # "Notifier" — MRO resolves left-to-right, depth-first (C3 linearization)
print(Service.__mro__)           # (Service, Notifier, Loggable, object)

When two parent classes define the same method, Python resolves the ambiguity via a deterministic algorithm (C3 linearization) rather than raising an error — which means the "winning" implementation depends on inheritance order (Notifier, Loggable vs. Loggable, Notifier), a subtlety that has caused real production bugs when a class's base order was "reorganized for readability" without realizing it changed runtime behavior.

TypeScript avoids this entire class of bug by disallowing multiple class inheritance outright — the mixin pattern (composing behavior via functions that return extended classes) is the idiomatic substitute, and it makes the composition explicit rather than implicit in inheritance order:

type Constructor<T = {}> = new (...args: any[]) => T;

function Notifiable<T extends Constructor>(Base: T) {
  return class extends Base {
    notify(): string { return "Notifiable"; }
  };
}

class BaseService {}
class Service extends Notifiable(BaseService) {}

Duck typing: real difference, not just terminology

[BEGINNER] "Duck typing" (if it walks like a duck and quacks like a duck, treat it as a duck) is often used loosely to describe both languages' structural interfaces, but the mechanism differs meaningfully:

  • TypeScript: structural typing is checked at compile time — if an object doesn't have the right shape, it's a compile error before the program ever runs.
  • Python: duck typing is resolved at call time — nothing checks whether an object has a .notify() method until that line actually executes; if it doesn't, you get a runtime AttributeError.
class FakeNotifier:
    pass

def send(notifier) -> None:
    notifier.notify()  # no error until this line actually runs

send(FakeNotifier())  # AttributeError: 'FakeNotifier' object has no attribute 'notify'

This is why Python codebases lean more heavily on tests and type checkers (mypy, pyright) to catch what TypeScript's compiler catches automatically — the design idea (interfaces as capabilities, Chapter 8) is identical, but the safety net around it differs by language, which should inform how much you rely on tests versus the type system in each.

When neither language's OOP toolkit is the right tool

[SENIOR INSIGHT] Returning to Chapter 1's central caveat with more specificity now that the full toolkit has been covered:

  • Data transformation pipelines — a chain of map/filter/reduce over immutable data is usually clearer in both languages than wrapping each transformation step in a class with a single method.
  • Configuration and DTOs — Python's @dataclass or a plain interface/type in TypeScript describes the shape without needing behavior; adding methods "to be object-oriented" when there's no behavior to protect just adds surface area.
  • Highly concurrent code — shared mutable objects are a liability under concurrency (fully covered in the companion Concurrency & Multithreading book); many concurrent designs favor immutable value objects and message passing over classes that manage protected-but-still-shared mutable state.
  • Simple scripts — a 30-line script that runs once doesn't need a class hierarchy; free functions operating on plain data are simpler to read and simpler to delete when the script is no longer needed.

The test from Chapter 1 still applies here in its most concrete form: is there state that needs protecting across multiple operations, with behavior tightly coupled to it? If not, plain functions and data structures — in either language — are very often the better design, not a compromise.

Common mistakes

  1. Writing Python as if it were Java/TypeScript with different syntax — e.g., adding getters/setters for every field out of habit, when Python's convention (_field plus a @property only when actual logic is needed) is idiomatically simpler.
  2. Writing TypeScript as if it were Python — e.g., relying on duck typing assumptions that the TypeScript compiler would actually have caught, or ignoring compiler errors by reaching for any, defeating the safety net structural typing provides.
  3. Using multiple inheritance in Python without understanding MRO, then being surprised when reordering base classes silently changes which implementation wins.
  4. Reaching for a class in either language when a plain function or dataclass/interface would be simpler and clearer — this is Chapter 1's lesson, restated now with two concrete languages' idioms to check against.

Checklist: idiomatic OOP in each language

  1. In TypeScript, use #field (not private) for any boundary that must be enforced at runtime, not just compile time.
  2. In Python, reach for @dataclass for data-holding classes, and reserve full manual __init__/__eq__ for cases needing custom logic.
  3. In Python, prefer Protocol over ABC for capability-style interfaces unless you specifically want to force explicit subclassing or share concrete implementation.
  4. If using Python's multiple inheritance, check ClassName.__mro__ whenever two parents might define the same method, rather than assuming resolution order.
  5. Before reaching for a class in either language, apply Chapter 1's test: is there state worth protecting, with tightly coupled behavior? If not, prefer functions and plain data structures.

Discussion / interview questions

  1. "What are the main practical differences between how TypeScript and Python implement OOP?" Strong answer: Covers at least three concrete differences from the table (access control enforcement, multiple inheritance support, compile-time vs. runtime structural typing) with why each matters in practice, not just a list of keywords.

  2. "What is Method Resolution Order, and why does it matter in Python but not in TypeScript?" Strong answer: Explains MRO (C3 linearization) as how Python resolves ambiguity when multiple parent classes define the same method, and notes TypeScript avoids the issue entirely by disallowing multiple class inheritance.

  3. "How does duck typing differ between Python and TypeScript, given both support 'structural' typing?" Strong answer: TypeScript checks structural compatibility at compile time (before the program runs); Python resolves it dynamically at the moment a method is actually called, risking a runtime AttributeError that a type checker or test would need to catch instead.

  4. "Give an example of code that would be worse if written in an object-oriented style in either language." Strong answer: A concrete example — a data pipeline, a DTO, a one-off script — where wrapping stateless logic or pure data in a class adds ceremony without protecting anything, tying back to Chapter 1's decision criteria.

  5. "You're reviewing a Python class using multiple inheritance from two mixins that both define save(). What would you check?" Strong answer: Checks the MRO (__mro__) to determine which save() actually runs, and flags the ambiguity for the team — especially if the base class order isn't clearly intentional or documented.

Chapter summary / cheat sheet

  • TypeScript and Python express identical OOP concepts with different mechanisms and different safety guarantees: compile-time (TypeScript) vs. convention/runtime (Python) enforcement, single vs. multiple inheritance, structural typing checked at compile time vs. duck typing resolved at call time.
  • Python's multiple inheritance is a real capability, but comes with MRO complexity that TypeScript avoids by disallowing it outright (using mixins instead).
  • Neither language's OOP toolkit is the right tool for stateless data pipelines, pure DTOs, highly concurrent shared state, or simple one-off scripts — Chapter 1's test (state worth protecting + tightly coupled behavior) still applies.
  • Write idiomatically in each language rather than transplanting one language's conventions into the other.