Patternsintermediateschedule14 min read

TypeScript vs Python: How Language Features Shape Design Pattern Implementations

Design patterns are like recipes — but the kitchen you cook in changes which tools you reach for. See how the same GoF patterns look radically different when Python's dynamic powers meet TypeScript's static precision.

Two snakes coiled on a white background.

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.

hourglass_empty

Rendering diagram...

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.

a close up of a curtain with a pattern on it

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.

Abstract grayscale textured pattern with wavy lines.

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:

PatternPython's ShortcutTypeScript's ShortcutClassic Version Needed?
StrategyFirst-class functionsTyped function signaturesRarely
SingletonModule system (auto-cached)Module scope + private constructorAlmost never
ObserverAny callable as listenerGeneric typed eventsConcept needed, ceremony isn't
Iteratoryield + __iter__ protocolyield + Symbol.iteratorNever in either language
Decorator@decorator syntaxHigher-order functions / class decoratorsOnly for complex wrapping
BuilderKeyword + default argumentsBuilder classes or option objectsPython 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.

Frequently Asked Questions

helpDo I still need design patterns if my language has first-class functions?

You still need the thinking behind patterns — they give you a shared vocabulary for recurring problems. But you often don't need the full class-based ceremony. In both Python and TypeScript, first-class functions collapse patterns like Strategy and Command into a single function variable. The pattern is still there conceptually; the implementation just gets smaller.

helpIs TypeScript's structural typing better than Python's nominal typing for design patterns?

Neither is universally better — they make different tradeoffs. Structural typing (TypeScript's default) means any object with the right shape automatically satisfies an interface, which makes patterns like Adapter and Strategy frictionless. Nominal typing (Python's default) gives you explicit intent — you know exactly which types were designed to work together. Python added Protocols (PEP 544) to get structural typing when you want it, so modern Python actually offers both styles.

helpWhich patterns are completely unnecessary in Python but still useful in TypeScript?

The classic Singleton pattern is unnecessary in Python because modules are natural singletons — import the same module twice and you get the same object. Builder is often unnecessary because Python's default and keyword arguments handle complex construction. In TypeScript, Singleton still sees use (though module-scoped instances work too), and Builder remains useful because TypeScript constructors can't use keyword arguments the same way Python can.

helpShould I learn the classic GoF versions of patterns before learning the idiomatic versions?

Yes, and here's why: the classic version teaches you the problem being solved and the structural thinking behind the solution. Once you understand why the Observer pattern exists (decoupling event producers from consumers), you'll recognize it whether it's implemented as a class hierarchy, a simple callback list, or a framework hook like React's useState. Learn the concept first, then learn your language's shortcut.

helpHow does Python's duck typing change the way I think about patterns compared to TypeScript's interfaces?

Duck typing means Python doesn't care what type something is — only what it can do. If it has a .calculate() method, it works as a Strategy. TypeScript's interfaces make this contract explicit and catch mismatches at compile time. The practical difference: Python patterns tend to be shorter (no interface declarations needed) but errors surface at runtime. TypeScript patterns are more verbose but the compiler catches mistakes before you run the code.

account_treeRelated Patterns