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 ๐ป
Who am I? ๐ค
Takanori Suzuki / ้ดๆจ ใใใฎใ ( @takanory)
PyCon JP Association: Vice Chair
BeProud Inc.: Director / Python Climber
Python Boot Camp, Python mini Hack-a-thon, Python Bouldering Club
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 ๐
Python Release Python 3.10.0
www.python.org/downloads/release/python-3100/
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' # 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 tobeer
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 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): # 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"
) tosize
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)
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
PEP 613: TypeAlias
# 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'