3 ways to build a SWITCH in python
Python is a language that has many different aspects when compared to other low level languages. It is often that I see C-like syntax in projects that I work on or even in old projects of my own. This is natural as most programmers usually start learning with low level languages and not always specialize on the most particular features that python has to offer.
The problem at hand
One of the most common subjects of question among python rookies is how to build a switch…case like pattern in python. If you are unaware of what I’m talking about, you can see below how the SWITCH...CASE
syntax looks like on the C language.
switch(color)
​{
case "blue":
printf("The sky is blue");
break;
case "yellow":
printf("The sun is yellow");
break;
case "red":
printf("Blood is red");
break;
default:
printf("I can't thing of anything with that color");
}
That is the reason why many people ask if python has a switch case syntax. That is totally understandable, as many low and high level languages have SWITCH
as a common keyword. However, python has no SWITCH
or CASE
statement as you know it. Therefore, very often we come to see the logic of that C snippet as if...else
trees
if color == "blue":
print("The sky is blue")
elif color == "yellow":
print("The sun is yellow")
elif color == "red":
print("Blood is red")
else:
print("I can't thing of anything with that color")
This might look as harmless for now, but in my experience, code grows. Worse: it tends to grow around the initial pattern they were built with! Thus, we come to see very long if...else
trees that would look really convoluted in the original SWITCH...CASE
syntax in C as well.
name = "Thomas Walker"
names = name.split()
if names[0] == "Arthur":
if names[1] == "Franklin":
initials = "A.F."
elif names[1] == "Goodman":
initials = "A.G."
elif names[1] == "Walker":
initials = "A.W."
else:
initials = "A."
elif names[0] == "Matthew":
if names[1] == "Franklin":
initials = "M.F."
elif names[1] == "Goodman":
initials = "M.G."
elif names[1] == "Walker":
initials = "M.W."
else:
initials = "M."
elif names[0] == "Peter":
if names[1] == "Franklin":
initials = "P.F."
elif names[1] == "Goodman":
initials = "P.G."
elif names[1] == "Walker":
initials = "P.W."
else:
initials = "P."
elif names[0] == "Thomas":
if names[1] == "Franklin":
initials = "T.F."
elif names[1] == "Goodman":
initials = "T.G."
elif names[1] == "Walker":
initials = "T.W."
else:
initials = "T."
else:
initials = None
The example above might seem useless, but the concept behind it is common while its structure is messy. Instead of getting initials
, imagine calling a specific function that would send and e-mail customized by family name. Moreover, if this is found on a crucial point in a large codebase, it could definitely harm the productivity and be considered technical debt for hindering the code comprehension.
Refactor: dict
The structure of the previous snippet recreates the pattern of the SWITCH
keyword present in other languages: it compares one value with another set of values until it finds a match or returns the default. In python, it is fairly simple to refactor the code above into something more readable with dict
.
switch = {
'Arthur': {
'Franklin': 'A.F.',
'Goodman': 'A.G.',
'Walker': 'A.W.'
},
'Matthew': {
'Franklin': 'M.F.',
'Goodman': 'M.G.',
'Walker': 'M.W.'
},
'Peter': {
'Franklin': 'P.F.',
'Goodman': 'P.G.',
'Walker': 'P.W.'
},
'Thomas': {
'Franklin': 'T.F.',
'Goodman': 'T.G.',
'Walker': 'T.W.'
},
}
names = "Thomas Walker".split()
initials = switch.get(names[0]).get(names[1], f"{names[0]}.")
As shown above, the simplest way to build the switch pattern in python is with the .get(key, default=None)
method in the dict
data structure. It can perform exactly what SWITCH...CASE
proposes, but in a much more readable fashion than if...else
trees.
I am not saying, however, that all if...else
statements could nor should be refactored into dictionaries. What I am doing is showing a pattern that is much easier to read and to edit that the previous one. We still have to consider the logic behind it.
This pattern can be very useful for function selection as well. Since functions are first-class citizens in python, we could build a library with several different implementations for a single concept. The example below presents the idea on how to do it with different fibonacci implementations:
def fibonacci_switch(key):
def loop(n):
...
def recurrent(n):
...
def memoization(n):
...
# you could refactor this as a loop
return {
loop.__name__: loop,
recurrent.__name__: recurrent,
memoization.__name__: memoization,
}.get(key, lambda: NotImplemented)
memoization_fibonacci = fibonacci_switch("memoization")
assert fibonacci(10) == 55
Refactor: dict
+ reduce
I find very common to hear that this pattern, however is too limited, especially when you have nested if trees. Since dict
can receive any type, even if we have a long nested tree, we might be able to refactor it as a dict
. Take, for instance, the crazy switch below.
switch = {
"noun": {
"animal": {
"mammal": "rabbit",
"reptile": "tortoise"
},
"human": "Napoleon Bonaparte",
},
"verb": {
"witchcraft": {
"summon",
"enchant",
"dispel"
},
"sports": {
"basketball": 5,
"soccer": 11,
"volleyball": 7,
"sumo": 2,
"darts": 1,
},
},
"adjective": {
"good": {
"excellent": 10,
"very good": 8,
"okay": 6,
},
"bad": {
"unbearable": 0,
"terrible": 2,
"awful": 4,
},
},
"adverb": ...,
}
For nested switches, what we want to define is a path
: it simply represents the sequence of keys for accessing each level. In our example, let’s make
path = ("verb", "sports", "sumo")
default = "not found"
Now, the most direct to reach our value would be to perform a loop as the one below. We can even add a try...except
block to leave as soon as we don’t receive a dict
(as only dict
has .get()
method in our switch).
item = switch
for key in path:
try:
item = item.get(key, default)
except AttributeError:
break
This is fair, I tell you, but let me show now an even simpler way to make the previous switch pattern even more readable.
from functools import reduce
result = reduce(lambda item, key: item.get(key, default), path, switch)
With the reduce
function available in the python standard library, we can make the previous for
loop an one-liner. It is true I’m cheating here, for there is no dict
checking with try..catch
, but it still works because the path
in this example is a valid one. You can refactor the anonymous lambda
function into a proper function though.
Refactor: __init_subclass__
The final SWITCH...CASE
refactor I want to show in python might come off as too much, but it is the one I believe to be the most elegant of all. Besides, I actually used it in a real world project! Therefore, I think it is important to present it here.
if fruit.kind == "apple":
result = actions.juice(fruit)
elif fruit.kind == "orange":
result = actions.cut(fruit)
elif fruit.kind == "banana":
result = actions.peel(fruit)
else:
raise ValueError(f"{fruit.kind} is not available")
energy = actions.eat(result)
if fruit.kind == "apple":
result = evaluate(energy, fruit)
elif fruit.kind == "orange":
result = evaluate(fruit=fruit)
elif fruit.kind == "banana":
result = evaluate(energy)
else:
raise ValueError(f"{fruit.kind} is not available")
process(result)
The if...else
tree above might seem very straightforward and maybe you find it unnecessary to refactor it at all. I argue however that you ought not see the code by what it is, but by what it can become. As a matter of fact, the idea of replicating this if...else
pattern will difficult the work of future contributors.
I believe that, with experience, a good developer would be able to see this pattern before it causes too much trouble and refactor it in a way it doesn’t hinder future productivity. For this example, we will refactor it as a class-based dispatcher with the dict
switch pattern. Besides, everything will be fairly simplified thanks to the awesome __init_subclass__
dunder method.
class BaseFruitModel():
registry = {}
def __init_subclass__(cls, fruit):
# register the child class as subclassed
BaseFruitModel.registry[fruit] = cls
The dunder init subclass magic method is called whenever the containing class is subclassed. cls
is then the new subclass. This allows us to register other classes in a single reference “dynamically”: We just need to subclass BaseFruitModel
and it will become a new branch in our switch.
class AppleModel(BaseFruitModel, fruit="apple"):
def make_juice(self):
...
return "apple juice"
class OrangeModel(BaseFruitModel, fruit="orange"):
def cut(self):
...
return "cut orange"
class BananaModel(BaseFruitModel, fruit="banana"):
def peel(self):
...
return "peeled 12 bananas"
Now, whenever I want to add a new fruit, I just need to come to the bottom of this file and add my new Fruit class subclassing BaseFruitModel
and setting a fruit. It is even possible to automate the the registry
key definition by using the class name as key with cls.__name__
if you want to.
class BaseRegistry():
registry = {}
def __init_subclass__(cls):
BaseFruitModel.registry[cls.__name__] = cls
Now, we still need to do a little more to actually be able to make the whole process functional. First, we add an Abstract Base Class to define our BaseFruitModel
as an interface. Then we can add some abstractmethods
and even a global method that calls the common interface in our registered models.
from abc import ABC, abstractmethod
class BaseFruitModel(ABC):
registry = {}
def __init_subclass__(cls, fruit):
# register the child class as subclassed
BaseFruitModel.registry[fruit] = cls
@abstractmethod
def prepare(self, fruit):
raise NotImplementedError
@abstractmethod
def evaluate(self, energy=None, fruit=None):
raise NotImplementedError
def eat(self, fruit):
fruits = self.registry.get(fruit).prepare(fruit)
return f"you did: {fruits}. Delicious!"
However, we have a lot of methods that share the same arguments. This is looks like it could be part of self
. Since fruit
is the only thing we need to initialize our classes, we can use dataclasses
to define the __init__
method automatically. a dataclass
requires we set a type for fruit
, I will use Mapping
here, but you could use Any
or a custom Fruit
class if it makes sense.
from dataclasses import dataclass
from typing import Hashable, Mapping, Optional
@dataclass
class BaseFruitModel(ABC):
fruit: Mapping
energy: Optional[str] = None
registry = {}
def __init_subclass__(cls, /, fruit: Hashable, **kwargs):
super().__init_subclass__(**kwargs)
# register the child class as subclassed
BaseFruitModel.registry[fruit] = cls
@abstractmethod
def prepare(self):
raise NotImplementedError
@abstractmethod
def evaluate(self):
raise NotImplementedError
def eat(self):
fruits = self.registry.get(self.fruit).prepare()
return f"you did: {fruits}. Delicious!"
For most cases, this approach might seem overkill, I know, but some time ago I had to use a pattern just like this while developing a function that would select a neural network model based on input data. As I was using class-based models in tensorflow
already, this BaseRegistry
class came as a mixin to each of my custom models. Then, it became much easier to perform the model selection.
Conclusions
if...else
trees are one of the most common code smells I find in python code. It is so simple to start an if
statement to select something that is also very common to keep it there forever.
On the other hand, python offers patterns better than the traditional switch case syntax in an easy-to-refactor style. Therefore, if you’ve seen convoluted if...else
trees in some code you’re working on, this is your chance to refactor it.
I hope you enjoyed this post and, as always, Happy Coding 🧑🏻‍💻