The Power of Multiple Dispatch in Python

The Secret Sauce Behind Julia and Lisp — Now in Python

The Power of Multiple Dispatch in Python
Photo by BHLNZ - Biodiversity Heritage Library NZ / Unsplash - Multiple Dispatch comes with Polymorphism — treating different combinations of function argument types in a different way — under the same function/method name

Have you ever wished your Python functions could respond more smartly — like really smartly — depending on all the argument types passed in? That’s multiple dispatch. It’s like function overloading’s cooler, more flexible cousin, and it’s built into languages like Julia and Common Lisp. But in Python? Well, it takes some extra work.

Let me take you on a little journey — story-first, code-always — to show you what works, what doesn’t, and what will save you time and sanity.

Part 1: Trying multipledispatch (And Seeing Where It Cracks)

Let’s say we’re writing a program that greets people differently based on whether they’re Vegan, Vegetarian, or just a regular Person. Sounds easy?

Let’s start with the multipledispatch package:

from multipledispatch import dispatch

class Person:
    pass

class Vegan(Person):
    pass

class Vegetarian(Person):
    pass

@dispatch(Vegan, str)
def greet(person, name):
    return f"Hi, I'm {name}! I'm a vegan!"

@dispatch(Vegetarian, str)
def greet(person, name):
    return f"Hi, I'm {name}! I'm a vegetarian!"

@dispatch(Person, str)
def greet(person, name):
    return f"Hi, I'm {name}! I'm a person!"

# Now test it
print(greet(Vegan(), "Holger"))
print(greet(Vegetarian(), "Knulf"))
print(greet(Person(), "Ralph"))

Output:

Hi, I'm Holger! I'm a vegan!
Hi, I'm Knulf! I'm a vegetarian!
Hi, I'm Ralph! I'm a person!

Perfect! Or so we thought…

Now let’s define a new class:

class Human(Person):
    pass

@dispatch(Human, str)
def greet(person, name):
    return f"Hi, I'm {name}! I'm a human!"

print(greet(Human(), "Bob"))

Expected: Hi, I’m Bob! I’m a human!

Actual:

Hi, I'm Bob! I'm a person!

Boom. It picked the more general match and completely ignored the more specific one. Why? Because multipledispatch resolves functions in the order they were defined and doesn’t search deeper.

This means it’s not actually multiple dispatch in the way we want it. It just grabs the first match it can find. Not great if you care about extensibility.


Part 2: Guido van Rossum to the Rescue (Sort of)

Thankfully, Guido van Rossum, Python’s creator, once suggested a DIY approach to multiple dispatch in a blog post.

Here’s the core of his idea:

registry = {}

class MultiMethod:
    def __init__(self, name):
        self.name = name
        self.typemap = {}

    def __call__(self, *args):
        types = tuple(arg.__class__ for arg in args)
        func = self.typemap.get(types)
        if func is None:
            raise TypeError("no match")
        return func(*args)

    def register(self, types, func):
        if types in self.typemap:
            raise TypeError("duplicate registration")
        self.typemap[types] = func

def multimethod(*types):
    def register(func):
        name = func.__name__
        mm = registry.setdefault(name, MultiMethod(name))
        mm.register(types, func)
        return mm
    return register

Let’s test this with our previous example:

class Person: pass
class Vegan(Person): pass
class Vegetarian(Person): pass

@multimethod(Vegan, str)
def greet(person, name):
    return f"Hi, I'm {name}! I'm a vegan!"

@multimethod(Vegetarian, str)
def greet(person, name):
    return f"Hi, I'm {name}! I'm a vegetarian!"

@multimethod(Person, str)
def greet(person, name):
    return f"Hi, I'm {name}! I'm a person!"

print(greet(Vegan(), "Holger"))
print(greet(Vegetarian(), "Knulf"))
print(greet(Person(), "Ralph"))

class Human(Person): pass

@multimethod(Human, str)
def greet(person, name):
    return f"Hi, I'm {name}! I'm a human!"

print(greet(Human(), "Bob"))

Output:

Hi, I'm Holger! I'm a vegan!
Hi, I'm Knulf! I'm a vegetarian!
Hi, I'm Ralph! I'm a person!
Hi, I'm Bob! I'm a human!

Nice. Works! Now try this:

class Scientist(Person): pass
print(greet(Scientist(), "John"))

Boom. Back to TypeError: no match. Because Guido’s approach does not do fallback to superclass matches. It expects exact matches only.

So now what? Time to level up and implement a real multiple dispatch system…


Part 3: Rolling Our Own Smarter Dispatcher

We’ve seen how multipledispatch fails to pick the most specific type and how Guido’s version fails to fall back to parent classes. Let’s fix that.

Here’s our plan: if there’s no exact match, we’ll search all possible parent type combinations (in order of specificity) until we find a match. This will give us a real multiple dispatch mechanism.

Let’s build it:

from functools import reduce

# Helper functions for searching fallback combinations

def penalize(groups, penalty=100):
    res = []
    for i, group in enumerate(groups):
        tmp = []
        for j in range(len(group)):
            tmp.append(i + penalty ** j)
        res.append(tmp)
    return res

def tuplize(groups, penalties):
    res = []
    for g, p in zip(groups, penalties):
        tmp = []
        for name, value in zip(g, p):
            tmp.append((name, value))
        res.append(tmp)
    return res

def as_tuple(groups):
    return tuplize(groups, penalize(groups))

def flatten1(lst):
    res = []
    for x in lst:
        if type(x) is list:
            res.extend(flatten1(x))
        else:
            res.append(x)
    return res

def _combine(lst1, lst2):
    return [flatten1((x, y)) for x in lst1 for y in lst2]

def combine(args):
    return reduce(_combine, args)

def de_tuplize_list(tuplized):
    group, penalties = list(zip(*tuplized))
    return group, sum(penalties)

def de_tuplize(tuplized_groups):
    return [de_tuplize_list(x) for x in tuplized_groups]

def sorted_combinations(groups):
    res = sorted(de_tuplize(combine(as_tuple(groups))), key=lambda x: x[1])
    return [comb for comb, penalty in res]

def get_parents(obj):
    if obj.__class__ is type:
        return [obj]
    else:
        return (obj.__class__).__mro__

def get_dispatch_combinations(args):
    _types = [get_parents(obj) for obj in args]
    return sorted_combinations(_types)

Now plug that into our improved MultiMethod:

registry = {}

class MultiMethod:
    def __init__(self, name):
        self.name = name
        self.typemap = {}

    def __call__(self, *args):
        types = tuple(arg.__class__ for arg in args)
        func = self.typemap.get(types)
        if func is None:
            for combo in get_dispatch_combinations(args):
                if combo in self.typemap:
                    func = self.typemap[combo]
                    break
            if func is None:
                raise TypeError("no match")
        return func(*args)

    def register(self, types, func):
        self.typemap[types] = func

def multimethod(*types):
    def register(func):
        name = func.__name__
        mm = registry.setdefault(name, MultiMethod(name))
        mm.register(types, func)
        return mm
    return register

Let’s test it:

class Person: pass
class Vegan(Person): pass
class Vegetarian(Person): pass
class Human(Person): pass
class Scientist(Person): pass

@multimethod(Vegan, str)
def greet(person, name):
    return f"Hi, I'm {name}! I'm a vegan!"

@multimethod(Vegetarian, str)
def greet(person, name):
    return f"Hi, I'm {name}! I'm a vegetarian!"

@multimethod(Person, str)
def greet(person, name):
    return f"Hi, I'm {name}! I'm a person!"

@multimethod(Human, str)
def greet(person, name):
    return f"Hi, I'm {name}! I'm a human!"

# Testing
print(greet(Vegan(), "Holger"))
print(greet(Vegetarian(), "Knulf"))
print(greet(Person(), "Ralph"))
print(greet(Human(), "Bob"))
print(greet(Scientist(), "John"))

Output:

Hi, I'm Holger! I'm a vegan!
Hi, I'm Knulf! I'm a vegetarian!
Hi, I'm Ralph! I'm a person!
Hi, I'm Bob! I'm a human!
Hi, I'm John! I'm a person!

Now even Scientist works! 


But Let’s Be Real — You Don’t Want to Maintain This

Yes, we did it. We created a real multiple dispatch engine in Python. But do you want to maintain this in production?

Didn’t think so.


Part 4: plum-dispatch — When Python Finally Behaves Like Julia

So far, we’ve seen:

  • multipledispatch: buggy and unpredictable
  • Guido’s version: educational but not production-safe
  • Our own smart dispatcher: works, but feels like writing a compiler in your spare time

But what if I told you there’s a Python library that already handles this the right way, supports type hints, inheritance-aware dispatch, and is actively maintained?

The solution in Python is: plum-dispatch.

Why plum-dispatch Rocks

  • Uses Python’s own type annotations
  • Picks the most specific matching method
  • Clean syntax: just @dispatch 
  • Allows you to define new methods for existing classes without touching them

Let’s dive right in.

Getting Started

Install it first:

uv add plum-dispatch

# or if you still are stuck with pip (learn `uv`!)
pip install plum-dispatch

A Familiar Example — Done Right

from plum import dispatch

class Person: pass
class Vegan(Person): pass
class Vegetarian(Person): pass

Now let’s define greet() for different types of people:

@dispatch
def greet(person: Vegan, name: str):
    return f"Hi, I'm {name}! I'm a vegan!"

@dispatch
def greet(person: Vegetarian, name: str):
    return f"Hi, I'm {name}! I'm a vegetarian!"

@dispatch
def greet(person: Person, name: str):
    return f"Hi, I'm {name}! I'm a person!"

And let’s run it:

print(greet(Vegan(), "Holger"))
print(greet(Vegetarian(), "Knulf"))
print(greet(Person(), "Ralph"))

Output:

Hi, I'm Holger! I'm a vegan!
Hi, I'm Knulf! I'm a vegetarian!
Hi, I'm Ralph! I'm a person!

Now add Human:

class Human(Person): pass

@dispatch
def greet(person: Human, name: str):
    return f"Hi, I'm {name}! I'm a human!"

print(greet(Human(), "Bob"))

It correctly outputs: Hi, I'm Bob! I'm a human!

Now add Scientist:

class Scientist(Person): pass

print(greet(scientist, "John"))

We didn't defined a method greet() for a Scientist. So it has to use the fallback to Person.

Output: Hi, I'm John! I'm a person!.

It correctly picks the fallback method for Scientist, since Scientist inherits from Person.

Now Let’s Go 2D: Add Another Method

Let’s define a meet() method that depends on both arguments — say, who meets whom.

We can write very specific cases:

@dispatch
def meet(p1: Person, p2: Person):
    return f"I'm a Person. Nice to meet you, Person!"

@dispatch
def meet(p1: Human, p2: Scientist):
    return f"I'm a Human. Nice to meet you, Scientist!"

@dispatch
def meet(p1: Scientist, p2: Human):
    return f"I'm a Scientist. Nice to meet you, Human!"

Let’s test them:

print(meet(Human(), Scientist()))
print(meet(Scientist(), Vegan()))
print(meet(Vegan(), Vegetarian()))

Output:

I'm a Human. Nice to meet you, Scientist!
I'm a Scientist. Nice to meet you, Person!
I'm a Person. Nice to meet you, Person!

Again, plum falls back to the most specific available method for the types given.

Now run:

print(meet(Vegan(), Vegetarian()))
print(meet(Vegan(), Human()))
print(meet(Human(), Scientist()))
print(meet(Scientist(), Vegan()))

Output:

I'm a Person. Nice to meet you, Person!
I'm a Person. Nice to meet you, Human!
I'm a Human. Nice to meet you, Scientist!
I'm a Scientist. Nice to meet you, Person!

This is more than just fun with decorators.

Multiple dispatch gives you:

  • Extensible systems that obey the Open-Closed Principle
  • Cleaner code: no endless if isinstance(…) chains
  • Easier testing and separation of concerns
  • Real polymorphism, beyond what class-based OOP offers

This is a superpower for data pipelines, simulations, AI model inference, type-safe DSLs, or any system where behavior should adapt to types at runtime.

Final Verdict

  • multipledispatch: avoid, buggy, abandoned
  • Guido’s DIY: fun, but no fallback logic
  • Hand-rolled improved dispatch: works, but maintainability nightmare
  • plum-dispatch: production-ready, expressive, elegant

Bonus Thought

This whole journey started because of Common Lisp and Julia (and R) — languages that treat code as data and dispatch as a core design pattern.

Python doesn’t have this natively, but with plum-dispatch, it almost feels like it does.

Happy multiple-dispatching!

And if you’re curious how Julia bakes this feature directly into the language, check out my intro to Julia here.

Let your functions finally grow up and handle real polymorphism.


Like this kind of thinking? I’ll be writing more soon on:
- Follow me on Medium @gwangjinkim for deep dives on Python, Lisp, system design, and developer thinking, and much more
- Subscribe on Substack: gwangjinkim.substack.com — coming soon with early essays, experiments & newsletters (just getting started).
- Follow my Ghost blog: everyhub.org — with hands-on tech tutorials and tools (most of them I cross-post here in medium, but not all of them).
Follow anywhere that fits your style — or all three if you want front-row seats to what’s coming next.

Maybe you also want to read:

Problem-Solving Like a Python Pro
A Hands-On Guide to Writing Clean, Flexible, and Future-Proof Python Code
A Julia Primer for Python Programmers
Learn Julia With Me!
Why Composability Is Such an Important Feature in Programming
Like inLego …

Multiple Dispatch in R:

Object-Oriented Programming in R: What S4 Can Do That S3 and Python Can’t
Learn How to Build Real DSLs and Frameworks Using R’s Most Misunderstood OOP System
How R’s S3 System Outshines Python’s Class Structures
The “primitive” S3 System as a polymorphic ninja.