# Switch-case statement in Python

- Date: 2018-10-20
- Tags: python
- Canonical URL: https://zablo.net/blog/post/python-switch-case-statement/


Have you ever wanted to use switch-case statement in Python code? If yes, then you have probably found out that there is no such expression in native Python syntax. Fortunately, you can implement it on your own while having some nerd-level fun. This post explains how to implement switch-case statement in Python.

## TL;DR
How to implement **switch-case** statement in Python, like this one:
```python
var = 666
with switch(var) as case:
    case(1, "foo")
    case(2, "yolo")
    case(3, "bar")
    case.default("dunno")
    result = case.result
print(result)
>>> "dunno"
```

## Prerequisites & system requirements
* can-do attitude
* liberal approach to Python
* time for some nerd-fun

## Switch-Case idea in Python
Here's the idea: Python has `with` statement, which can be used to control entering and exiting to the blocks of code. This is done by implementing `__enter__` and `__exit__` magic functions in your object.

Another magic function that is pretty handy is `__call__`. This one allows to handle object "calling", which is basically this:

```python
x = MyClass()
x() # <-- __call__ method of object x of type MyClass will be executed there
```

OK - we have our building blocks. As you've probably saw in the *TL;DR* above, our desired syntax is the following:

```python
with switch(SOME_OBJECT) as case:
    case(1, "case value for 1")
    case(2, "case value for 2")
    case.default(r"¯\_(ツ)_/¯")
    result = case.result
print(result)
```

Additionaly, I would like to have option for case condition and case result to be `lambda` expression, like this:

```python
y = object()
with switch(y) as case:
    case(lambda: isinstance(y, str), "Some characters")
    case(lambda: isinstance(y, int), "Numberzzzz")
    case(lambda: isinstance(y, float), "Why u not double?")
    case.default(r"¯\_(ツ)_/¯")
    result = case.result
print(result)
```

## Switch-Case implementation in Python
Without further ado, here's the implementation:

```python
class switch(object):
    def __init__(self, object_to_switch_on):
        self.object_to_switch_on = object_to_switch_on
        self.default_result = None
        self.result_store = None
        self.cases = {}
        self.evaluated = False

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        _ = self.result

    def __call__(self, when, then):
        if when in self.cases:
            raise ValueError("This case already exists")
        else:
            self.cases[when] = then
        return self

    def default(self, then):
        self.default_result = then
        return self

    @property
    def result(self):
        if not self.evaluated:
            self.result_store = self.__evaluate__()
        return self.result_store

    def __evaluate__(self):
        from inspect import signature
        self.evaluated = True
        result_tmp = None
        if all(callable(x) for x in self.cases.keys()):
            for case_fn, case_result in self.cases.items():
                no_of_params = len(signature(case_fn).parameters)
                if no_of_params >= 2:
                    raise ValueError("Case function must have either 0 or 1 arguments")

                if no_of_params == 1 and case_fn(self.object_to_switch_on):
                    result_tmp = case_result
                    break
                elif no_of_params == 0 and case_fn():
                    result_tmp = case_result
                    break
            else:
                result_tmp = self.default_result
        elif all(not callable(x) for x in self.cases.keys()):
            result_tmp = self.cases[self.object_to_switch_on] if self.object_to_switch_on in self.cases else self.default_result
        else:
            raise ValueError("Inconsistent switch usage, use either simple objects or functions")
        if result_tmp is not None and callable(result_tmp):
            return result_tmp()
        else:
            return result_tmp
```

### Code explanation
1. Class name is lowercased in order to imitate basic language syntax while used.
1. Construtor (`__init__`) takes "switchable" object.
1. Magic functions: `__enter__` & `__exit__` are here to implement Python `with` scope behaviour. Exit calls `result` property in order to trigger the evaluation when exiting the scope (it's here just to shorten the usage syntax with lambdas even more, see exmaples below).
1. Magic function `__call__` is used to add cases to our switch as we call it. Cases are stored internally in a dictionary for fast direct object access (as long as the case is a hashable object). 
1. `default` method adds default case and it's optional to call it.
1. `result` property getter is triggering switch-case evaluation. If it's called for the first time, it invokes `__evaluate__` method, which does all the magic of resolving switch-case statement result. Every subsequent call returns the result from cache.
1. `__evaluate__` method is where all of the meat is. This method checks if all of the cases are either callable objects (most likely functions or lambdas). If they are, it iterates through them and assumes that match is first match for which the function/lambda returned *truthy* value. If none of the cases is callable, I assume that the objects are hashable and I just lookup for their values in the internal cases dictionary. After resolving the case result, I also check, whether result is callable - if it is, I invoke it, if not I return it.

## Summary
And that's it. Don't rate the *Pythonic* code ratio. Like I said, it's only for liberal Python programmes. Hope you liked it!

## Additional links & resources
* full project on my github: <a href="https://github.com/marrrcin/python-switch-case-statement" target="_blank">https://github.com/marrrcin/python-switch-case-statement</a>


