A switch-case statement is smarter than writing lots of large if-statements instead, so does Python wants us to mess up our code with the worse alternative?
Definitely not! Despite the fact that switch-statements are indicators for code smell and you should be careful of using them anyway, there is a clever way to make up for the lack of switch statements in Python!
Disclaimer: Python 3.10 introduced structural pattern matching which might also be an alternative for the switch statement depending on the use-case. However, this post is focussed on an alternative that works with all versions of Python 3, if you want a dedicated post for the new pattern matching please let me know in the comments!
What is the Switch-Statement?
The switch statement is a control flow for comparing a value with multiple variants. In general, this is just syntactic sugar for a large if-statement. The syntax may look like this:
switch (x) {case 'variant1':// do something here if x == 'variant1'break;case 'variant2':// do something here if x == 'variant2'default:// do something here if no case matched// or no break statement jumped out of the switch before}
This syntax applies to nearly all common programming languages which support the switch-statement, like Java, JavaScript, or C for instance. Note that you have to use a break if you want the program to leave the switch statement after a case matched! Otherwise, the program will continue running the instructions for the next case, without checking if the case actually matches.
The Code Smell of Switch-Statements
The previous explanation already showed one of the most common pitfalls with switch statements: The syntax can easily lead to unintended bugs when you mess something up with the break-statements.
Besides that, you can do anything within the body of a case. That means every case can have a different behavior which may be really unintentional. For instance, you can stop a loop in the above statements or return something and exit the current function. This can get really messy when your project grows!
Additionally, the switch-statement is not optimal for code that is supposed to get extended in the future. Every time you want to add a case, you have to modify the statement. Depending on the language you are using, you maybe need to redeploy the whole application every time you want to add some functionality. This clearly violates the Open-Closed Principle!
Alternative in Python
If you want to check a value for many possibilities, you clearly want something like the switch-statement. The worst thing you can do is to solve the problem with tons of if-statements that pollute your code – you should never do that! But the good thing is: There's a smart pythonic approach to compensate for the switch-statement!
In general, the pattern is based on the enormous power of dictionaries in Python. As an example, we take a look into a simple command-line game. The game will just ask you for a decision at each step. In this context, the dictionary mapping gives you the power to do something like this:
direction_messages = {"north": "You freeze to death","east": "You die in the desert after 42 days of suffering","south": "You win the Superbowl!","west": "You fall into a gigantic ravine and die instantly",}direction = input("where do you want to go? ")message = direction_messages.get(direction.strip().lower(),"You have to give a compass direction")print(message)
In this example, the value for which we check the cases is the user's input and the cases are represented as keys of the dictionary. Note that do the dictionary lookup with the get() function, which every dictionary provides. The advantage is, that you can specify a default value with the second parameter. This corresponds to the default case of the switch statement and we don't have to catch a KeyError, in case the user gives an invalid answer.
Adding functionality to the cases
Okay, this covers basically the functionality of a switch-statement, but we only have strings as values. Indeed, the switch-statement allows you to run any code you want for a case. To achieve this, we can use a dictionary of functions. At first, this may seem a bit strange, but we can use function objects as dictionary values too! With this technique, we can create a mapping for functions that define the blocks of code for each case.
But first, let me start with the easiest version: lambda functions. They are simply single-expression and anonymous functions. They take as many arguments as you want, but can only execute one expression and return its result. A simple example for using lambdas in this context would be a basic calculator:
operations = {"+": lambda x, y: x + y,"-": lambda x, y: x - y,"*": lambda x, y: x * y,"/": lambda x, y: x / y,}operation = input("Which operation should I execute? ")operation_function = operations.get(operation,lambda x, y: "unsupported operation")numbers = input("Numbers for the calculation: ")numbers_list = numbers.split(' ')result = operation_function(*list(map(int, numbers_list)))print(f"Result: {result}")
Although this is just a very basic and a little bit ugly calculator you can do much better for sure, it shows how you can use lambdas for simple operations. This time, we get a callable object instead of a string from the dictionary. This callable is stored in a variable, which we can call with the numbers the user gives the calculator.
We can use functions too!
Though the lambdas can be really useful, in most cases you want to do more than one expression. Sure, you can call a function in a lambda expression – but you can also replace the lambda with a function right away. Then you have a function for every case, where you can do everything you want. Now we want to expand our little command-line game from before.
def player_goes_north():pass # do here whatever you wantdef player_goes_east():pass # do here whatever you wantdef player_goes_south():pass # do here whatever you wantdef player_goes_west():pass # do here whatever you wantdef unknown_direction():print("You have to enter a compass direction!")direction_messages = {"north": player_goes_north,"east": player_goes_east,"south": player_goes_south,"west": player_goes_west,}direction = input("where do you want to go? ")perform_player_action = direction_messages.get(direction.strip().lower(),unknown_direction)perform_player_action()
Now we can do anything we want for each way the player wants to go. It works the same way as before, we just replaced the lambdas with normal functions.
A common pitfall, especially when one isn't familiar with the concept of functions as objects, might be to put parentheses behind the function names in the dictionary. Make sure to only have the name in there. You don't want to call the function, you only want the function object stored in the dictionary.
There are a few more possibilities to use dictionaries with functions, but these are the basic ones that should suffice most of the time. Finally, I want to point out the fact that you can use any mutable object as a key in dictionaries, not only strings like in the previous examples. You can even use data-types for it and much more! So you have great opportunities to take advantage of this pattern!
Conclusion
In Python, you can easily create a good alternative for the switch-statement in a really pythonic way. I personally think that this way is even better than the switch, it has almost none of its disadvantages.
Although, there is even another approach. There is an object-oriented alternative that uses polymorphism to get rid of the switch-statement. This pattern is particularly common in other languages where you cannot use the pythonic approach. If you're interested in this alternative or have questions about this topic, leave a comment and I do my best to explain it!