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 💻
Who am I? 👤
Takanori Suzuki / 鈴木 たかのり ( @takanory)
PyCon JP Association: Chair
BeProud Inc.: Director / Python Climber
Python Boot Camp, Python mini Hack-a-thon, Python Bouldering Club
Me and Korea 🇰🇷
Attended PyCon APAC 2016 in Korea
PyCon APAC 2023 in Tokyo, Japan 🇯🇵
Date: 2023 Oct 27-29
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 🆕
Python Release Python 3.10.11 🐍
www.python.org/downloads/release/python-31011/
Who are You? 🐍
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
PEPs for 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 tobeer
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 tobeer
、"nuts"
assign tofood
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 !! 🙏
@takanory takanory takanory takanory