Introduction to Structural Pattern Matching

Takanori Suzuki

PyCon APAC 2021 / 2021 Nov 20

Agenda

  • Motivation

  • What’s New

  • Syntax

  • Patterns

Photos 📷 Tweets 🐦 👍

#pyconapac / @takanory

Slide 💻

slides.takanory.net

Who am I? 👤

../_images/sokidan-square.jpg

Motivation of this talk 💪

  • Structural Pattern Matching looks useful

  • You to know and try 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

Have you used 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.0

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

Python Release Python 3.10.0

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'  # OK
>>> class = 'class'  # NG
  File "<stdin>", line 1
    class = 'class'  # NG
          ^
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:
    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:
    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:
    case "Pilsner":
        result = "First drink"
    case "IPA":
        result = "I like it"
    case "Hazy IPA":
        result = "Cloudy and cloudy"
    # case _:
    #     result = "I like most beers"

? 🤔

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 ("", ""):  # match here
            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_beer_and_food(("", ""))  # -> 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, ""):  # match here
            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_beer_and_food(("IPA", ""))  # -> 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):  # match here
            return f"I drink {beer} with {food}."
        case _:
            return "one beer and one food only."

order_beer_and_food(("IPA", "nuts"))  # -> 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 _:  # match here
            return "one beer and one food only."

order_beer_and_food(("IPA", "nuts", "spam"))  # -> 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 do you like?

  • Structural Pattern Matching

  • if statement

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_beer_and_food(("IPA", ""))  # -> I drink IPA with .

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."

Order 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

Classes patterns

  • With multiple classes

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 multiple patterns

  • 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

  • Matching specific attions(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 are "Pint" and "HalfPint"

  • "beer IPA 1-liter" is invalid

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

Capturing matched sub-patterns

  • Use as patterns

  • Assign the size value("Pint" or "HalfPint") to size

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

Matching multiple values

  • Can handle multiple food order

  • example:

    • "food nuts fries pickles"

order_text = "food nuts fries pickles"

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

Matching multiple values

  • Add * to variable name

order_text = "food nuts fries pickles"

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

Mapping Patterns 📕

Mapping Patterns 📕

  • Pattern match for dictinaries

  • 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": str(style), "size": _}:
        print("Unknown beer size")
    case {"water": int(number)}:
        grass_of_water(number)
    case {"bill": _}:
        calculate_amount()

Guards 💂‍♀️

Guards 💂‍♀

  • if statement after pattern

order_list = ["water", 3]  # -> 3 glasses of water, please.
# order_list = ["water", 15]  # -> You can only order 1-9 glasses of water.

match order_list:
    case ["water", int(number)] if 0 < number < 10:
        print(f"{number} glasses of water, please.")
    case ["water", _]:
        print("You can only order 1-9 glasses of water.")

Summary

Summary

  • Motivation 💪

  • Syntax

    • Soft keywords: match, case and _

  • Patterns

    • Literal, Variable, Classes, Sequense, Mapping

    • Wildcard, OR, AS, Guards

Try Structural Pattern Matching 👍

References 📚

Thank you !! 🙏

Takanori Suzuki ( @takanory)

slides.takanory.net

../_images/sokidan-square.jpg

What’s New in Python 3.10 🆕

What’s New in Python 3.10 🆕

  • Parenthesized Context Managers 👈

  • Better Error Messages 👈

  • Better Typing Syntax 👈

  • Structural Pattern Matching

  • Better Debugging

Parenthesized Context Managers

# 3.10
with (
    open('craftbeer.txt') as f1,
    open('beer-in-kanda.txt') as f2,
):
    ...
# Before 3.10
with open('craftbeer.txt') as f1, \
     open('beer-in-kanda.txt') as f2
    ...

Better Error Messages

# Brackets are not closed
beer_types = ['Pilsner', 'Ale', 'IPA', 'Hazy IPA'
print(beer_types)
$ python3.10 beer_styles.py
  File ".../beer_styles.py", line 2
    beer_styles = ['Pilsner', 'Ale', 'IPA', 'Hazy IPA'
                  ^
SyntaxError: '[' was never closed
# Easy to understand!!
$ python3.9 beer_styles.py
  File ".../beer_styles.py", line 3
    print(beer_styles)
    ^
SyntaxError: invalid syntax

Better Error Messages

# 3.10
>>> if beer_syle = 'IPA':
  File "<stdin>", line 1
    if beer_syle = 'IPA':
       ^^^^^^^^^^^^^^^^^
SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='?
# Very friendly!!
# Before 3.10
>>> if beer_syle = 'IPA':
  File "<stdin>", line 1
    if beer_syle = 'IPA':
                 ^
SyntaxError: invalid syntax

Better Typing Syntax

  • PEP 604: New Type Union Operator

    • Union[X, Y] → X | Y

    • Optional[X] → X | None

# 3.10
def drink_beer(number: int | float) -> str | None
    if am_i_full(number):
        return 'I'm full'
# Before 3.10
def drink_beer(number: Union[int, float]) -> Optional[str]
    if am_i_full(number):
        return 'I'm full'

Better Typing Syntax

# 3.10
BeerStr: TypeAlias = 'Beer[str]'  # a type alias
LOG_PREFIX = 'LOG[DEBUG]'  # a module constant
# Before 3.10
BeerStr = 'Beer[str]'  # a type alias
LOG_PREFIX = 'LOG[DEBUG]'  # a module constant

Better Typing Syntax

  • Can use Python 3.7 - 3.9

from __future__ import annotations

def drink_beer(number: int | float) -> str | None
    if am_i_full(number):
        return 'I'm full'

Try Python 3.10 👍