Introduction to Structural Pattern Matching

Takanori Suzuki

PyCon KR 2023 / 2023 Aug 12

Agenda

  • Motivation / Goal

  • What’s New

  • Syntax

  • Patterns

Photos 📷 Tweets 🐦 👍

#pyconkr / @takanory

Slide 💻

slides.takanory.net

slides.takanory.net

Who am I? 👤

takanory profile kuro-chan and kuri-chan

Me and Korea 🇰🇷

  • Attended PyCon APAC 2016 in Korea

PyCon APAC 2016

PyCon APAC 2023 in Tokyo, Japan 🇯🇵

../_images/pyconapac2023-logo.png

Motivation of this talk 💪

  • Structural Pattern Matching looks useful

  • You to know and use it

Goal of this talk 🥅

  • Learn syntax and basic usage

  • Learn various patterns and how to use them

  • Try it tomorrow

Prerequisites

  • Intermediate level

  • You should know Python syntax

    • tuple, list, dict, if, def, isinstance, dataclass, type hinting and more

Questions

Are you using Python 3.10+? 🙋‍♂️

Do you know the new features? 🙋‍♀️

What’s New in Python 3.10 🆕

What’s New in Python 3.10 🆕

What's New in Python 3.10

Python Release Python 3.10.11 🐍

www.python.org/downloads/release/python-31011/

Python Release Python 3.10.11

Who are You? 🐍

Python 3.10 release logo

New features of Python 3.10

  • Parenthesized Context Managers

  • Better Typing Syntax

  • Better Error Messages

  • Structural Pattern Matching

  • Better Debugging

New features of Python 3.10

  • Parenthesized Context Managers

  • Better Typing Syntax

  • Better Error Messages

  • Structural Pattern Matching 👈

  • Better Debugging

Structural Pattern Matching

Structural Pattern Matching

Motivation

www.python.org/dev/peps/pep-0635/#motivation

(Structural) pattern matching syntax is found in many languages, from Haskell, Erlang and Scala to Elixir and Ruby. (A proposal for JavaScript is also under consideration.)

Motivation

# check type or shape of an object
if isinstance(x, tuple) and len(x) == 2:
    host, port = x
    mode = "http"
elif isinstance(x, tuple) and len(x) == 3:
    host, port, mode = x
# Structural Pattern Matching
match x:
    case host, port:
        mode = "http"
    case host, port, mode:
        pass

Syntax

  • Generic syntax of pattern matching

match subject:
    case <pattern_1>:
        <action_1>
    case <pattern_2>:
        <action_2>
    case <pattern_3>:
        <action_3>
    case _:
        <action_wildcard>

Soft keywords

  • New in Python 3.10

  • match, case and _

  • Can be used identifier names

>>> match = "match"  # Valid(Soft keyword)
>>> class = "class"  # Invalid(Keyword)
  File "<stdin>", line 1
    class = "class"
          ^
SyntaxError: invalid syntax

Patterns

Patterns

match subject:
    case <pattern_1>:
        <action_1>
    case <pattern_2>:
        <action_2>
    case <pattern_3>:
        <action_3>
    case _:
        <action_wildcard>

Literal patterns

match beer_style:  # "Pilsner" / "IPA" / "Ale"
   case "Pilsner":
       result = "First drink"
   case "IPA":
       result = "I like it"
   case "Hazy IPA":
       result = "Cloudy and cloudy"
   case _:
       result = "I like most beers"

OR patterns

  • | is OR

match beer_style:  # "IPA" / "Session IPA"
    case "Pilsner":
        result = "First drink"
    case "IPA" | "Session IPA":
        result = "I like it"
    case "Hazy IPA":
        result = "Cloudy and cloudy"
    case _:
        result = "I like most beers"

Literal patterns without wildcard

match beer_style:  # "Ale"
    case "Pilsner":
        result = "First drink"
    case "IPA":
        result = "I like it"
    case "Hazy IPA":
        result = "Cloudy and cloudy"
    # case _:
    #     result = "I like most beers"

Useful? 🤔

rewrite with if statement

  • If written as an if statement

if beer_style == "Pilsner":
    result = "First drink"
elif beer_style == "IPA" or beer_style == "Session IPA":
    result =  "I like it"
elif beer_style == "Hazy IPA":
    result = "Cloudy and cloudy"
else:
    result = "I like most beers"

Pattern Matching is Powerful 💪

Literal and Variable patterns

Literal and Variable patterns

def order_beer_and_food(order: tuple) -> str:
    match (order):
        case ("", ""):
            return "Please order something."
        case (beer, ""):
            return f"I drink {beer}."
        case ("", food):
            return f"I eat {food}."
        case (beer, food):
            return f"I drink {beer} with {food}."
        case _:
            return "one beer and one food only."

Literal and Variable patterns

def order_beer_and_food(order: tuple) -> str:
    match (order):
        case ("", ""):
            return "Please order something."
        case (beer, ""):
            return f"I drink {beer}."
        case ("", food):
            return f"I eat {food}."
        case (beer, food):
            return f"I drink {beer} with {food}."
        case _:
            return "one beer and one food only."

order = ("", "")
print(order_beer_and_food(order))  # -> Please order something.

Literal and Variable patterns

  • "IPA" assign to beer

def order_beer_and_food(order: tuple) -> str:
    match (order):
        case ("", ""):
            return "Please order something."
        case (beer, ""):
            return f"I drink {beer}."
        case ("", food):
            return f"I eat {food}."
        case (beer, food):
            return f"I drink {beer} with {food}."
        case _:
            return "one beer and one food only."

order = ("IPA", "")
print(order_beer_and_food(order))  # -> I drink IPA.

Literal and Variable patterns

  • "IPA" assign to beer"nuts" assign to food

def order_beer_and_food(order: tuple) -> str:
    match (order):
        case ("", ""):
            return "Please order something."
        case (beer, ""):
            return f"I drink {beer}."
        case ("", food):
            return f"I eat {food}."
        case (beer, food):
            return f"I drink {beer} with {food}."
        case _:
            return "one beer and one food only."

order = ("IPA", "nuts")
print(order_beer_and_food(order))  # -> I drink IPA with nuts.

Literal and Variable patterns

  • Tuple length does not match

def order_beer_and_food(order: tuple) -> str:
    match (order):
        case ("", ""):
            return "Please order something."
        case (beer, ""):
            return f"I drink {beer}."
        case ("", food):
            return f"I eat {food}."
        case (beer, food):
            return f"I drink {beer} with {food}."
        case _:
            return "one beer and one food only."

order = ("IPA", "nuts", "spam")
print(order_beer_and_food(order))  # -> one beer and one food only.

rewrite with if statement

def order_beer_and_food(order: tuple) -> str:
    if len(order) == 2:
        beer, food = order
        if beer == "" and food == "":
            return "I'm full."
        elif beer != "" and food == "":
            return f"I drink {beer}."
        elif beer == "" and food != "":
            return f"I eat {food}."
        else:
            return f"I drink {beer} with {food}."
    else:
        return "one beer and one food only."

Which code do you prefer?

  • Pattern Matching 🆚 if statement

Case Order is important ⬇️

def order_beer_and_food(order: tuple) -> str:
    match (order):
        case (beer, food):  # match here
            return f"I drink {beer} with {food}."
        case ("", ""):  # never reach
            return "Please order something."
        case (beer, ""):  # never reach
            return f"I drink {beer}."
        case ("", food):  # never reach
            return f"I eat {food}."
        case _:
            return "one beer and one food only."

order = ("", "nuts")
print(order_beer_and_food(order))  # -> I drink  with nuts.

Classes patterns

Classes patterns

@dataclass
class Order:  # Order(beer="IPA"), Order("Ale", "nuts")...
    beer: str = ""
    food: str = ""
def order_with_class(order: Order) -> str:
    match (order):
        case Order(beer="", food=""):
            return "Please order something."
        case Order(beer=beer, food=""):
            return f"I drink {beer}."
        case Order(beer="", food=food):
            return f"I eat {food}."
        case Order(beer=beer, food=food):
            return f"I drink {beer} with {food}."
        case _:
            return "Not an order."

Results: Classes patterns

>>> order_with_class(Order())
'Please order something.'
>>> order_with_class(Order(beer="Ale"))
'I drink Ale.'
>>> order_with_class(Order(food="fries"))
'I eat fries.'
>>> order_with_class(Order("Ale", "fries"))
'I drink Ale with fries.'
>>> order_with_class("IPA")
'Not an order.'

Classes patterns

def order_with_class(order: Order) -> str:
    match (order):
        case Order(beer="", food=""):
            return "Please order something."
        case Order(beer=beer, food=""):
            return f"I drink {beer}."
        case Order(beer="", food=food):
            return f"I eat {food}."
        case Order(beer=beer, food=food):
            return f"I drink {beer} with {food}."
        case _:
            return "Not an order."

rewrite with if statement

def order_with_class(order: Order) -> str:
    if isinstance(order, Order):
        if order.beer == "" and order.food == "":
            return  "Please order something."
        elif order.beer != "" and order.food == "":
            return f"I drink {order.beer}."
        elif order.beer == "" and order.food != "":
            return f"I eat {order.food}."
        else:
            return f"I drink {order.beer} with {order.food}."
    else:
        return "Not an order."

Use multiple classses

@dataclass
class Beer:  # Beer("IPA", "Pint")
    style: str
    size: str

@dataclass
class Food:  # Food("nuts")
    name: str

@dataclass
class Water:  # Water(4)
    number: int

Use multiple classses

def order_with_classes(order: Beer|Food|Water) -> str:
    match (order):
        case Beer(style=style, size=size):
            return f"I drink {size} of {style}."
        case Food(name=name):
            return f"I eat {name}."
        case Water(number=number):
            return f"{number} glasses of water, please."
        case _:
            return "Not an order."

rewrite with if statement

def order_with_classes(order: Beer|Food|Water) -> str:
    if isinstance(order, Beer):
        return f"I drink {order.size} of {order.style}."
    elif isinstance(order, Food):
        return f"I eat {order.name}."
    elif isinstance(order, Water):
        return f"{order.number} glasses of water, please."
    else:
        return "Not an order."

Sequense patterns ➡️

Sequense patterns ➡️

  • Parse the order text

  • for example:

order_text = "beer IPA pint"
order_text = "food nuts"
order_text = "water 3"
order_text = "bill"

Matching by length of sequence

match order_text.split():
    case [action]:  # match "bill"
        ...
    case [action, name]:  # match "food nuts", "water 3"
        ...
    case [action, name, size]:  # match "beer IPA pint"
        ...

Matching specific values in sequence

  • Specific values: "bill", "food"

match order_text.split():
    case ["bill"]:  # match "bill"
        calculate_amount()
    case ["food", food]:  # match "food nuts"
        tell_kitchen(food)
    case ["water", number]:  # match "water 3"
        grass_of_water(number)
    case ["beer", style, size]:  # match "beer IPA pint"
        tell_beer_master(style, size)

Capturing matched sub-patterns

  • Valid beer size: "Pint" or "HalfPint"

  • "beer IPA Small" is invalid

order_text = "beer IPA Pint"

match order_text.split():
    ...
    case ["beer", style, ("Pint" | "HalfPint")]:
        # I don't know beer size

Capturing matched sub-patterns

  • Use as patterns

  • ("Pint" | "HalfPint") as size

order_text = "beer IPA Pint"

match order_text.split():
    ...
    case ["beer", style, ("Pint" | "HalfPint") as size]:
        tell_beer_master(style, size)  # size is "Pint"

Matching multiple values

  • I want to order several foods

  • example: "food nuts fries pizza"

order_text = "food nuts fries pizza"

match order_text.split():
    ...
    case ["food", food]:  # capture single value
        tell_kitchen(food)

Matching multiple values

  • food*foods

order_text = "food nuts fries pizza"

match order_text.split():
    ...
    case ["food", *foods]:  # capture multiple values
        for food in foods:  # ("nuts", "fries", "pizza")
            tell_kitchen(food)

I can order several foods!! 🎉

Mapping Patterns 📕

Mapping Patterns 📕

  • Pattern match for dict

  • Useful for alalyzing JSON

order_dict = {"beer": "IPA", "size": "Pint"}

match order_dict:
    case {"food": food}:
        tell_kitchen(food)
    case {"beer": style, "size": ("Pint" | "HalfPint") as size}:
        tell_beer_master(style, size)
    case {"beer": style, "size": _}:
        print("Unknown beer size")
    case {"water": number}:
        grass_of_water(number)
    case {"bill": _}:
        calculate_amount()

Matching builtin classes

  • Use str(), int() and more

order_dict = {"water": 3}
# order_dict = {"water": "three"}  # Doesn't match

match order_dict:
    case {"food": str(food)}:
        tell_kitchen(food)
    case {"beer": str(style), "size": ("Pint" | "HalfPint") as size}:
        tell_beer_master(style, size)
    case {"beer": style, "size": _}:
        print("Unknown beer size")
    case {"water": int(number)}:
        grass_of_water(number)
    case {"bill": _}:
        calculate_amount()

Guards 💂‍♀️

Number of water

  • Invalid: 9, 10, …

  • Valid: 1, 2, … 8

  • Invalid: 0, -1, …

Guards 💂‍♀

  • Valid: 1, 2, … 8

  • if statement after pattern

order_list = ["water", 3]  # -> 3 glasses of water, please.
# order_list = ["water", 15]  # -> 15 is invalid value.

match order_list:
    case ["water", int(number)] if 1 <= number <= 8:
        print(f"{number} glasses of water, please.")
    case ["water", _]:
        print(f"{number} is invalid value.")

Can’t order many glasses of water! 🎉

Summary

Summary

  • Motivation / Goal

  • What’s New

  • Syntax

    • Soft keywords: match, case and _

  • Patterns

    • Literal, Variable, Classes, Sequense, Mapping

    • Wildcard(_), OR(|), AS, Guards

Try Structural Pattern Matching 👍

References 📚

Thank you !! 🙏

slides.takanory.net

@takanory takanory takanory takanory

takanory profile kuro-chan and kuri-chan