The switch-case is an essential statement which is  widely adopted in most programming languages . It is used in a situation where we want to map some  defined actions depending on a given expression.

In most languages, the switch-case statement looks as shown below:

switch(expression){
    case pattern1:
        //do this
        break;
    case pattern2:
        //do this
        break;
    case pattern3:
        //do this
        break;
    default:
        //do this
}

The first case block whose pattern  matches the result of the expression gets executed.

The default block is  executed if the given expression did not match any of the patterns defined by the  case blocks.

Until version 3.10, Python didn't have an equivalent to the popular switch-case statements . Python developers had to use other means to simulate the working of the switch-case such as by using the if statement or a dictionary. These  methods have their own downsides since there are some cases where the native switch-case fits well. 

The following example shows how these may be achieved with if statement:

def switch(lang):
   if lang == "English":
      print("How are you?")
   elif lang == "French":
      print("comment allez-vous?")
   elif lang == "German":
      print("Wie geht es dir?")
   elif lang == "Swahili":
      print("Habari yako?") 

switch("Swahili")

and with dictionaries

switch = {
    "English":"How are you?",
    "French":"comment allez-vous?",
    "German":"Wie geht es dir?",
    "Swahili":"Habari yako?"
}

print(switch["German"])

match, case and _  keywords 

From Python 3.10 onwards, three keywords match ,  case and  _   have been introduced to offer builtin support  for feature equivalent to switch-case  in other languages. Python does not yet treat them as other keywords and they are referred to as soft keywords.  As of now, you can use these keywords as identifiers without raising a syntax error which will happen if you use the "normal" keywords. You  can view these keywords using the softkwlist attribute  of the builtin keyword module.

import keyword
print(keyword.softkwlist) #

How they work

The three keywords( match , case and  ) are now the standard way to go when you want to implement a similar functionality as switch case in other language, their syntax is as follows:

match term:
   case pattern1:
        #do this
   case pattern2:
        #do this
   case pattern3:
        #do this
   case _:
        default action

The underscore symbol which is also among the  soft keywords   is used to define the default case for the match  statement, the statements under it's case block will get executed only if all the preceding cases fail to match the term. 

In some languages like C++, you can only apply switch statements on integers but in Python you can do it with any data type

Our greetings  example  can now be written in the most standard  way as follows,

def match(lang):
   match lang:
       case "English":
          print("How are you?")
       case "French":
          print("Comment allez-vous?")
       case "German":
          print("Wie geht es dir?")
       case "Swahili":
          print("Habari yako?") 

match("French")      

 Use match-case to print whether a number is odd or even.

num = 8
match num % 2:
    case 1:
        print("Odd")
    case 0:
        print("Even")

The following example uses the match case block to map an operator to an expression. 

def evaluate(num1, num2, oper):
    num1, num2 = int(num1), int(num2)

    result = None
    match oper:
        case "+":
            result = num1 + num2
        case "-":
            result = num1 - num2
        case "*":
            result = num1 * num2
        case "/":
            result = num1 / num2
        case "%":
            result = num1 % num2
        case "**":
            result = num1 ** num2
        case _:
            return ("Invalid operator '%s'"%oper)

    return f"{num1} {oper} {num2} = {result}"

print(evaluate(100,10,"+"))
print(evaluate(3,5,"*"))
print(evaluate(100,10,"/"))
print(evaluate(5,5, "**"))
print(evaluate(100,10,"@"))

Matching  types/classes

The match-case statement can also be used to match the type of the given value, this way, the case statements whose expression evaluates to the type of the value will be executed. The following is a basic example:

value = {1, 2, 3, 4}

match value:
   case int():
      print(f"{value} is an integer.")
   case str():
      print(f"{value} is string.")
   case set():
      print(f"{value} is a set.")
   case list():
      print(f"{value} is a list.")

In the above example, value is a set, therefore, the third case statement's expression matches and gets executed.

Note the parentheses following the name of those types i.e int(), str(), set() and list(), the parentheses are necessary when matching classes, remember that builtin types are classes. We are actually instantiating an object of that types and then its attributes are matched with that of the given value. Not using the parentheses with class expressions may lead to a SyntaxError exception being raised.

this raises a syntax error. 

value = {1, 2, 3, 4}

match value:
   case int:
      print(f"{value} is an integer.")
   case str:
      print(f"{value} is string.")
   case set:
      print(f"{value} is a set.")
   case list:
      print(f"{value} is a list.")

If the constructor of the class that the value belong take arguments, we can pass arguments in the case expressions  to match specific objects. Consider the following example:

class Point:
   def __init__(self, x = 0, y = 0):
      self.x = x
      self.y = y
   def __str__(self):
      return f"Point({self.x}, {self.y})"


p = Point(x = 2, y = 4)

match p:
   case Point(x = 0, y = 0):
      print(f"First case matched, {p}")
   case Point(x = 1, y = 2):
      print(f"Second case matched, {p}")
   case Point(x = 2, y = 4):
      print(f"Third case matched, {p}")
   case Point(x = 5, y = 10):
      print(f"Fourth case matched, {p}")
   case _:
      print("NO MATCHES")

Obviously, the  above usage assumes that the constructor of the class retains attributes.

Unpacking in case expressions

When the value to be matched is an iterable we can use a syntax similar to unpacking. This allows us to use aliasing and other unpacking features as well as other advanced unique features, consider the following example:

seq = (7, 8)

match seq:
   case (1, 2):
      print('First case matched!')
      print(f"seq = {(1, 2)}")
   case (2, b):
      print('Second case matched!')
      print(f"seq = {(2, b)}")
   case (a, 8):
      print('Third case matched!')
      print(f"seq = {(a, 8)}")
   case _:
      print('NO MATCHES!')

Note carefully the expressions of the case blocks in the above examples. In the first case block we used all known values i.e (1, 2), meaning  this block will only match if seq's values are strictly 1 and 2. But in the second and third case block's expressions, we used aliasing. In the second case's expression, we passed only the first value and then used alias b for the second value, this tells the match statement to assign b to the value of the second element in seq. The same is true with the third case statement where we only passed the second  value and aliased the first as a, thus the match statement assigns a to the value of the first element in seq.

To understand the above usage better, check the following example:

seq = (7, 8, 3)

match seq:
   case (2, b, c):
      print('First case matched!')
      print(f"seq = {(2, b, c)}")
   case (7, b, c):
      print('Second case matched!')
      print(f"seq = {(7, b, c)}")
   case (4, b, c):
      print('Third case matched!')
      print(f"seq = {(4, b, c)}")
   case _:
      print('NO MATCHES!')

In the above example, we aliased the second and the third values for all case statements, thus the only defining factor is the first value which were entered manually.

If you find the above usage hard to grasp, you should first familiarize yourself with how unpacking works here. It is also possible to use more advanced unpacking features in the case expressions when you understand unpacking.

case expression with an if clause

We can embed  an if clause in a case condition to make the match more strict for that particular case block. If the pattern matches the case block but the condition specified by if fails, the match will go on to the next case block.

def evaluate(num1, num2, oper):
    num1, num2 = int(num1), int(num2)

    result = None
    match oper:
        case "+":
            result = num1 + num2
        case "-":
            result = num1 - num2
        case "*":
            result = num1 * num2
        case "/" if num2 != 0:
            result = num1 / num2
        case "%":
            result = num1 % num2
        case "**":
            result = num1 ** num2
        case _:
            return ("Invalid operator '%s' with operands %s and %s"%(oper, num1, num2))

    return f"{num1} {oper} {num2} = {result}"

print(evaluate(100,10,"/"))
print(evaluate(100, 0,"/"))

In the above example we used the if clause in the case of "/" operator to make the match go on  if num2 is equal to 0 . This ensures division by zero is not carried out hence avoiding the ZeroDivisionError exception.