Python has been designed to be a very readable and compact programming language since the language was born in 1991. Indentation is required, semicolons are not needed, readability and elegance are highly promoted and I'm sometimes still stunned how complex use-cases can be implemented so clean and simple with just a few lines of Python.
But one of the things that has been controversial in the language, and a hot topic in the Python community for more than 30 (!) years, is the lack of a switch
or case
statement.
The Python official FAQ wrote about this:
You can do this easily enough with a sequence of if... elif... elif... else
.
switch
statementIn 2001, PEP-275 was submitted for Python 2 to introduce a switch
statement in the language, but this was never accepted.
In 2006, Guido himself submitted the introduction of a case
statement in PEP-3103, but this was also rejected (by himself) because this proposal did not have "popular support".
It did take up to 2020 for PEP-634 and PEP-635 to be proposed which introduces a case
statement, including advanced structural pattern matching as found in Haskell and Ruby.
In October 2021, a match...case
statement was finally introduced in Python 3.10.
Let's see how it looks like with some examples:
For the following examples, we'll use a Response
class which has two properties, a status_code
(like 200
or 404
) and an error_code
, such as "invalid-credentials"
:
@dataclass class Response: status_code: int error_code: str
If we would make a handler to react to a specific response, we would traditionally write it like this:
def handle_response(response): if response.status_code == 400: return BadRequestHandler(response) elif response.status_code == 403: error_message = response.body.json()["message"] return UnauthorizedHandler(response, message=error_message) elif response.status_code == 500: return ServerErrorHandler(response) else: return UnknownStatusCodeHandler(response)
It does the job, but still it looks a bit messy due to the different spacing, and this kind of code just screams for a better alternative.
For a simple handling mechanism, you could use a dictionary to map the status_code
to a specific handler:
def handle_response(response): handler_mapping = { 400: BadRequestHandler, 403: UnauthorizedHandler, 500: ServerErrorHandler, } handler = handler_mapping.get( response.status_code, UnknownStatusCodeHandler) return handler(response)
Although this looks already much cleaner compared to the if... elif... else
example above, it has some limitations:
Rewriting the first example with the new match...case statement, it will look like this:
def handle_response_match(response): match response.status_code: case 400: return BadRequestHandler(response) case 403: error_message = response.body.json()["message"] return UnauthorizedHandler(response, message=error_message) case 500: return ServerErrorHandler(response) case _: return UnknownStatusCodeHandler(response)
It's hard to believe that this hasn't been added to the language earlier! These kind of multi-condition-statements are much more readable as the alternatives we were used too, and makes the implementation of these kind of handlers so much easier.
And there is even more, as the match
statement can do some advanced matching like this:
response = { "status_code": 403, "error_code": "invalid-credentials" } def handle_response(response): match response: case {"status_code": 403, "error_code": "invalid-credentials"}: # This handles a specific 403 response return InvalidCredentialsHandler(response) case {"status_code": 403, "error_code": error_code}: # This handlers all other 403 responses return BadRequestHandler(response, error_code) ...
Or even like this:
def handle_response(response): match response: case Response(403, "invalid-credentials"): return InvalidCredentialsHandler(response) case Response(400, error_code): return NotFoundHandler(error_code) ...
When the response
object would be a tuple you can even unpack the values like this:
def handle_response(response): match response: case (404, error_code): return NotFoundHandler(error_code) case (403, "invalid-credentials"): return InvalidCredentialsHandler(response) case (403, error_code): return UnauthorizedHandler(response, error_code) ...
And you can even add more conditions with guards:
def handle_response(response, user): match response: case (403, error_code) if user.authenticated: return NoPermissionHandler(response) case (403, error_code): return UnauthorizedHandler(response, error_code) ...
Go and check out PEP-636, which contains even more great and advanced examples what you can do with structural pattern matching in Python.
If you didn't find a good reason to update to Python 3.10 before, there is one now!