You Learned the Patterns — Now They Look Different in Every Language
Here's a scenario that trips up every developer who learns design patterns from a textbook: you study the Strategy pattern using Java examples, feel confident, then open a Python codebase and can't find a single Strategy class anywhere. But the pattern is there — it's just wearing different clothes.
That's because design patterns aren't fixed code templates — they're solutions shaped by the language you write them in. A pattern that requires four classes in Java might need one function in Python and a typed interface in TypeScript. Same problem, same concept, wildly different implementation.
Peter Norvig famously demonstrated this in 1996 when he showed that 16 of the 23 Gang of Four patterns become "invisible or simpler" in dynamic languages. The patterns didn't disappear — the language absorbed them.
This article puts TypeScript and Python side by side for five essential patterns. For each one, we'll see the classic textbook version, the idiomatic version that leans into each language's strengths, and a verdict explaining why they differ. Think of it as a lab experiment: same recipe, two different kitchens.
The Two Kitchens: A Quick Language Comparison
Before we dive into patterns, let's understand what makes these two kitchens different.
Python's kitchen has a built-in food processor (first-class functions and dynamic typing), a spice rack that labels itself (duck typing), and countertops that rearrange on demand (metaprogramming via descriptors and metaclasses). You can throw ingredients together quickly, and the kitchen trusts you to know what you're doing.
TypeScript's kitchen has precision measuring tools (static types), labeled containers that reject the wrong ingredients (interfaces and generics), and an assistant who checks your recipe before you start cooking (the compiler). It takes slightly longer to set up, but mistakes get caught before anything hits the oven.
The key technical distinction: TypeScript uses structural typing (if it has the right shape, it fits the type), while Python defaults to nominal typing (types must match by name). Python later added structural typing via PEP 544 Protocols, but the default mindset of each language shapes how developers write patterns.
Pattern 1: Strategy — "Swap the Algorithm"
The Problem: Your app calculates discounts. Regular customers get 10% off, premium members get 20% off, and employees get 30% off. You need to swap the discount algorithm based on the customer type.
The Classic Textbook Version (Both Languages)
// Classic Strategy — works in BOTH languages
// Requires an interface/abstract class + concrete implementations
INTERFACE DiscountStrategy:
calculate(price) → number
CLASS RegularDiscount IMPLEMENTS DiscountStrategy:
calculate(price):
RETURN price * 0.10
CLASS PremiumDiscount IMPLEMENTS DiscountStrategy:
calculate(price):
RETURN price * 0.20
CLASS EmployeeDiscount IMPLEMENTS DiscountStrategy:
calculate(price):
RETURN price * 0.30
CLASS PriceCalculator:
PRIVATE strategy: DiscountStrategy
setStrategy(strategy):
this.strategy = strategy
getDiscount(price):
RETURN this.strategy.calculate(price)
That's three classes and an interface just to multiply by different numbers. Let's see how each language simplifies this.
Idiomatic Python: Just Use a Function
// Python — Strategy collapses to a function variable
// No interface. No classes. Just pass a function.
FUNCTION regular_discount(price):
RETURN price * 0.10
FUNCTION premium_discount(price):
RETURN price * 0.20
FUNCTION employee_discount(price):
RETURN price * 0.30
// The "context" is just a function that accepts a function:
FUNCTION calculate_price(price, discount_func):
discount = discount_func(price)
RETURN price - discount
// Usage — swap strategy by passing a different function:
final = calculate_price(100, premium_discount) // 80
Python's first-class functions and duck typing make the entire class hierarchy vanish. If the function takes a price and returns a number, it's a valid strategy. No interface declaration needed.
Idiomatic TypeScript: A Typed Function Signature
// TypeScript — Strategy as a typed function, not a class
// The interface IS the function signature:
TYPE DiscountStrategy = (price: number) => number
CONST regularDiscount: DiscountStrategy = (price) => price * 0.10
CONST premiumDiscount: DiscountStrategy = (price) => price * 0.20
CONST employeeDiscount: DiscountStrategy = (price) => price * 0.30
FUNCTION calculatePrice(price: number, strategy: DiscountStrategy): number
RETURN price - strategy(price)
// Usage — same simplicity, but the compiler ensures type safety:
calculatePrice(100, premiumDiscount) // 80
calculatePrice(100, "not a function") // COMPILE ERROR!
Verdict
Both languages collapse Strategy from a class hierarchy to a function variable — first-class functions are the key feature. The difference? TypeScript adds a typed contract (DiscountStrategy type) that catches mistakes at compile time. Python trusts you to pass the right kind of function and tells you at runtime if you didn't. Same simplification, different safety nets.
Pattern 2: Singleton — "Only One Instance"
The Problem: Your app needs exactly one configuration manager that loads settings from a file. Creating multiple instances would be wasteful and could cause inconsistent state.
Idiomatic Python: The Module IS the Singleton
// Python — you don't need a Singleton pattern at all
// A module IS a singleton. Import it twice? Same object.
// FILE: config.py
_settings = LOAD settings FROM "config.yaml" // Runs ONCE on first import
FUNCTION get(key):
RETURN _settings[key]
FUNCTION set(key, value):
_settings[key] = value
// FILE: anywhere_in_your_app.py
IMPORT config
config.get("database_url") // Always the same settings object
Python's module system caches modules after the first import. Every file that imports config gets the exact same object. The Singleton pattern is literally built into how Python loads code.
Photo by Olga Kovalski on Unsplash
Idiomatic TypeScript: Module Scope Gets You Most of the Way
// TypeScript — module-scoped instance (the common approach)
// FILE: config.ts
CONST settings: Record<string, string> = LOAD from "config.yaml"
EXPORT FUNCTION get(key: string): string
RETURN settings[key]
EXPORT FUNCTION set(key: string, value: string): void
settings[key] = value
// Works the same way — module is cached after first import
But TypeScript also lets you enforce the Singleton contract at the type level when you need a class-based approach:
// TypeScript — class-based Singleton with private constructor
// The compiler PREVENTS anyone from calling "new" directly
CLASS ConfigManager:
PRIVATE STATIC instance: ConfigManager | undefined
PRIVATE settings: Map<string, string>
PRIVATE CONSTRUCTOR(): // <-- compiler enforces this!
this.settings = LOAD from "config.yaml"
STATIC getInstance(): ConfigManager
IF NOT ConfigManager.instance:
ConfigManager.instance = NEW ConfigManager()
RETURN ConfigManager.instance
Verdict
Python's module system makes Singleton a non-pattern — you get it for free. TypeScript offers the same module-caching behavior, but also gives you a class-based option with private constructor that enforces the "only one instance" rule at compile time. Python has no way to make a constructor truly private — it relies on convention (the underscore prefix) rather than enforcement.
Pattern 3: Observer — "Notify Everyone When Something Changes"
The Problem: When a user updates their profile, you need to update the avatar in the navbar, refresh the activity feed, and sync to the server. These concerns shouldn't know about each other.
Idiomatic Python: Callbacks and Callables
// Python — Observer via simple callback list
// Any callable works as a listener — functions, methods, lambdas
CLASS EventEmitter:
PRIVATE _listeners = DICTIONARY of (event_name → list of functions)
on(event, callback): // Subscribe
_listeners[event].APPEND(callback)
emit(event, data): // Notify
FOR EACH callback IN _listeners[event]:
callback(data) // Duck typing — just call it!
// Usage — any function with the right signature works:
emitter = NEW EventEmitter()
emitter.on("profile_updated", update_navbar) // A function
emitter.on("profile_updated", feed.refresh) // A bound method
emitter.on("profile_updated", LAMBDA data: sync(data)) // A lambda
Python's duck typing means anything callable is a valid observer. Functions, methods, lambdas, even objects with a __call__ method — they all work without implementing any interface.
Idiomatic TypeScript: Typed Events with Generics
// TypeScript — Observer with generic type safety
// The compiler ensures event names and data types match
TYPE EventMap = {
"profile_updated": { name: string, avatar: string }
"order_placed": { orderId: string, total: number }
}
CLASS TypedEmitter<Events>:
PRIVATE listeners = MAP of (event_name → list of callbacks)
on<K extends keyof Events>(event: K, callback: (data: Events[K]) => void):
listeners[event].PUSH(callback)
emit<K extends keyof Events>(event: K, data: Events[K]):
FOR EACH callback IN listeners[event]:
callback(data)
// Usage — the compiler knows EXACTLY what data each event carries:
emitter = NEW TypedEmitter<EventMap>()
emitter.on("profile_updated", (data) => {
// TypeScript KNOWS data has .name and .avatar
updateNavbar(data.name, data.avatar)
})
emitter.on("profile_updated", (data) => {
data.orderId // COMPILE ERROR — profile_updated doesn't have orderId!
})
Verdict
Python's duck typing makes Observer dead simple — any callable works. TypeScript's generics and mapped types add a layer that Python can't match at the language level: the compiler knows which events exist and what data they carry. Misspell an event name or access the wrong property? TypeScript catches it before your code runs. Python catches it when your user reports a bug.
Pattern 4: Iterator — "Walk Through a Collection"
The Problem: You have a custom tree data structure and want to traverse it with a simple for loop, just like you'd loop through a list.
Idiomatic Python: The Yield Keyword
// Python — Iterator via generator functions
// yield makes ANY function an iterator automatically
CLASS TreeNode:
value, left, right
// This single method makes TreeNode work with for-loops:
__iter__(self): // Python's "iterator protocol"
IF self.left:
YIELD FROM self.left // Recursively yield left subtree
YIELD self.value // Yield this node
IF self.right:
YIELD FROM self.right // Recursively yield right subtree
// Usage — it just works with for-loops, list(), and all built-ins:
FOR value IN tree:
PRINT value
sorted_values = LIST(tree) // Collect into a list
first_three = LIST(TAKE(3, tree)) // Lazy — only traverses 3 nodes
Python's yield keyword and iterator protocol (__iter__, __next__) mean the Iterator pattern is absorbed into the language. You don't implement it — you speak the language's protocol, and everything that works with loops works with your object.
Photo by MARIOLA GROBELSKA on Unsplash
Idiomatic TypeScript: Symbol.iterator and Generators
// TypeScript — Iterator via Symbol.iterator + generator
// Similar to Python, but with explicit typing
CLASS TreeNode<T>:
value: T
left?: TreeNode<T>
right?: TreeNode<T>
// Makes TreeNode iterable with for...of loops:
*[Symbol.iterator](): Generator<T>
IF this.left:
YIELD FROM this.left
YIELD this.value
IF this.right:
YIELD FROM this.right
// Usage — works with for...of and spread:
FOR (CONST value OF tree):
console.log(value)
CONST sorted = [...tree] // Spread into an array
Verdict
This is the rare case where both languages handle the pattern almost identically. Both use generator functions (yield/yield from) and a protocol-based approach (__iter__ in Python, Symbol.iterator in TypeScript/JavaScript). The Iterator pattern has been fully absorbed into both languages. TypeScript adds generic type safety (TreeNode yields T values), but the core mechanism is the same.
Pattern 5: Decorator (the Pattern, Not the Syntax) — "Add Behavior Without Changing the Original"
The Problem: You have an API client that fetches data. Sometimes you need to add caching. Sometimes logging. Sometimes retry logic. You want to mix and match these behaviors without modifying the original client.
This is where the two languages diverge most dramatically, because Python has decorator syntax (@decorator) built into the language.
Idiomatic Python: The @ Decorator Syntax
// Python — Decorator pattern via actual decorator syntax
// The language has SYNTACTIC SUPPORT for this pattern
FUNCTION with_cache(func):
cache = {}
FUNCTION wrapper(*args):
IF args IN cache:
RETURN cache[args] // Cache hit!
result = func(*args) // Call original
cache[args] = result
RETURN result
RETURN wrapper
FUNCTION with_logging(func):
FUNCTION wrapper(*args):
PRINT "Calling", func.name, "with", args
result = func(*args)
PRINT "Returned", result
RETURN result
RETURN wrapper
// Stack decorators with @ syntax — reads top-to-bottom:
@with_logging
@with_cache
FUNCTION fetch_user(user_id):
RETURN API.get("/users/" + user_id)
// fetch_user now AUTOMATICALLY has caching AND logging
fetch_user(42) // Logs the call, checks cache, fetches if needed
Python decorators are so natural that most Python developers don't even think of them as a "design pattern" — they're just how you write Python.
Idiomatic TypeScript: Wrapper Functions or Class Decorators
// TypeScript — Decorator via higher-order functions
// Similar to Python, but without the @ sugar for standalone functions
TYPE AsyncFn<T> = (...args: any[]) => Promise<T>
FUNCTION withCache<T>(fn: AsyncFn<T>): AsyncFn<T>
CONST cache = NEW Map()
RETURN ASYNC (...args) => {
CONST key = JSON.stringify(args)
IF cache.has(key):
RETURN cache.get(key)
CONST result = AWAIT fn(...args)
cache.set(key, result)
RETURN result
}
FUNCTION withLogging<T>(fn: AsyncFn<T>): AsyncFn<T>
RETURN ASYNC (...args) => {
console.log("Calling with", args)
CONST result = AWAIT fn(...args)
console.log("Returned", result)
RETURN result
}
// Compose manually (no @ syntax for standalone functions):
CONST fetchUser = withLogging(withCache(ASYNC (id: string) => {
RETURN AWAIT api.get("/users/" + id)
}))
TypeScript now has TC39 Stage 3 decorators for class methods, but standalone function decoration still requires manual composition — which reads inside-out rather than top-to-bottom.
Verdict
Python wins this round on ergonomics. The @decorator syntax makes the Decorator pattern a first-class language feature — it reads naturally and stacks cleanly. TypeScript can do the same thing with higher-order functions, and its new standard decorators work well on class methods, but it requires more mental gymnastics for function composition. Both languages use the same underlying mechanism (wrapping functions), but Python makes it feel native.
The Big Picture: When Language Features Absorb Patterns
Let's zoom out. Here's a cheat sheet of which language features replace or simplify which patterns:
| Pattern | Python's Shortcut | TypeScript's Shortcut | Classic Version Needed? |
|---|
| Strategy | First-class functions | Typed function signatures | Rarely |
| Singleton | Module system (auto-cached) | Module scope + private constructor | Almost never |
| Observer | Any callable as listener | Generic typed events | Concept needed, ceremony isn't |
| Iterator | yield + __iter__ protocol | yield + Symbol.iterator | Never in either language |
| Decorator | @decorator syntax | Higher-order functions / class decorators | Only for complex wrapping |
| Builder | Keyword + default arguments | Builder classes or option objects | Python rarely; TypeScript sometimes |
Notice the trend: Python's dynamic features absorb patterns by making the solution implicit, while TypeScript's static features absorb patterns by making the contract explicit. Neither approach is strictly better — they optimize for different kinds of safety.
When the Kitchen Matters More Than the Recipe
Here's what experienced polyglot developers know: don't port patterns literally between languages. A Singleton class in Python is a code smell. An untyped Observer in TypeScript is a missed opportunity. The pattern catalog is a concept library, not a code library.
When you move between Python and TypeScript, ask yourself:
- Does my language already have this pattern built in? (Python modules = Singleton, generators = Iterator)
- Can a language feature simplify the implementation? (First-class functions = simpler Strategy in both)
- What safety guarantees do I lose or gain? (TypeScript generics catch Observer bugs at compile time; Python's duck typing makes Observer setup faster)
The Gang of Four gave us a vocabulary for talking about solutions. Your language gives you the grammar for expressing them. Learn both, and you'll write code that's not just correct — it's native to whatever kitchen you're cooking in.
As the saying goes: patterns are timeless. Their code is not.