Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[PyconPL 2024] Having fun with Pydantic and pat...

Sebastian Buczyński
August 31, 2024
49

[PyconPL 2024] Having fun with Pydantic and pattern matching

Sebastian Buczyński

August 31, 2024
Tweet

Transcript

  1. Sebastian Buczyński • Software Architect @ Sauce Labs • Trainer

    / Consultant @ Bottega IT Minds • I like memes • Blogger @ breadcrumbscollector.tech • Educate Pythonistas about software engineering whoami
  2. match latte: case Coffee() : print("It's caffee latte or latte

    machiato!") case Tea() : print("It's Matcha Latte") case _: print("Huh, no idea what it is”)
  3. match latte: case Coffee() : print("It's caffee latte or latte

    machiato!") case Tea() : print("It's Matcha Latte") case _: print("Huh, no idea what is it")
  4. match latte: case Coffee() : print("It's caffee latte or latte

    machiato!") case Tea() : print("It's Matcha Latte") case _: print("Huh, no idea what is it")
  5. price = 200 match price: case 100 : print("Cheap!") case

    200 : print("Expensive!") case _: print("No idea")
  6. price = 200 match price: case 100 : print("Cheap!") case

    200 : print("Expensive!") case _: print("No idea")
  7. price = 200 match price: case 100 : print("Cheap!") case

    200 : print("Expensive!") case _: print("No idea")
  8. price = 200 match price: case 100 : print("Cheap!") case

    200 : print("Expensive!") case _: print("No idea")
  9. price = 200 match price: case 100 : print("Cheap!") case

    200 : print("Expensive!") case _: print("No idea") What we’ll see? #1
  10. price = 2137 match price: case 100 : print("Cheap!") case

    200 : print("Expensive!") case _: print("No idea") What we’ll see? #2
  11. data = (1, 2, 3) match data: case {} :

    print("It's a dict!") case tuple() : print("Huh, a tuple!") case _: print("No idea") What we’ll see? #3
  12. data = {"name": "Sebastian"} match data: case {} : print("It's

    a dict!") case tuple() : print("Huh, a tuple!") case _: print("No idea") What we’ll see? #4
  13. What we’ll see? #5 data = {"name": "Sebastian"} match data:

    case {"name": _} : print("That's a dict with 'name'") case {} : print("It's a dict!") case _: print("No idea")
  14. What we’ll see? #6 data = {"name": "Sebastian"} match data:

    case {} : print("It's a dict!") case {"name": _} : print("That's a dict with 'name'") case _: print("No idea")
  15. What we’ll see? #6 data = {"name": "Sebastian"} match data:

    case {} : print("It's a dict!") case {"name": _} : print("That's a dict with 'name'") case _: print("No idea") Ordering of ‚cases’ is signi f icant
  16. Match and assign name data = {"name": "Sebastian"} match data:

    case {"name": name} : print(f"That's a dict with 'name' = {name}") case _: print("No idea")
  17. Match multiple types using union data = {"price": 123} match

    data: case {"price": int() | float()} : print(f"That thing has a price tag on it!") case _: print("No idea")
  18. Match multiple patterns using union data = {"price": 123} match

    data: case {"name": _} | {"price": _} : print(f"It has a name or a price!") case _: print("No idea")
  19. Aliases on patterns data = {"name": "sebastian"} match data: case

    {"name": str() as name} : print(f"It has a name - {name}!") case _: print("No idea")
  20. guard clauses with „if” data = {"name": "sebastian"} match data:

    case {"name": str() as name} if name[0] == name[0].upper() : print(f"Hello! {name}!") case _: print("Name should start with a capital letter, mate!")
  21. Consuming stream of events: data {"type": "AccountCreated", "id": "64160ccb", "name":

    "Matthew"} {"type": "AccountUpdated", "id": "bb915f85", "old_status": "trial", "new_status": "paid"} {"type": "AccountDeleted", "id": "57ae55c5"}
  22. Consuming stream of events: data {"type": "AccountCreated", "id": "64160ccb", "name":

    "Matthew"} {"type": "AccountUpdated", "id": "bb915f85", "old_status": "trial", "new_status": "paid"} {"type": "AccountDeleted", "id": "57ae55c5"} {"type": "AccountUpdated", "id": "fede79b2", "old_status": "paid", "new_status": "trial"} {"type": "AccountCreated", "id": "23826148", "name": "John"}
  23. Consuming stream of events: data {"type": "AccountCreated", "id": "64160ccb", "name":

    "Matthew"} {"type": "AccountUpdated", "id": "bb915f85", "old_status": "trial", "new_status": "paid"} {"type": "AccountDeleted", "id": "57ae55c5"} {"type": "AccountUpdated", "id": "fede79b2", "old_status": "paid", "new_status": "trial"} {"type": "AccountCreated", "id": "23826148", "name": "John"} {"username": "Hecker"}
  24. def handle(payload: dict) -> None: # code like it's 2020

    😎 if payload.get("type") == "AccountCreated": ... elif payload.get("type") == "AccountDeleted": ... elif payload.get("type") == "AccountUpdated": ... else: print("Omg, what is this?!")
  25. def handle(payload: dict) -> None: # code like it's 2020

    😎 # ... and pretend we're in control 🙈🙉🙊 if payload.get("type") == "AccountCreated": ... elif payload.get("type") == "AccountDeleted": ... elif payload.get("type") == "AccountUpdated": if payload["new_status"] == "paid": ... elif payload["new_status"] == "trial": ... else: print("Omg, what is this?!")
  26. def handle(payload: dict) -> None: # HAMMER TIME! 🔨 match

    payload: case _: if payload.get("type") == "AccountCreated": ... elif payload.get("type") == "AccountDeleted": ... elif payload.get("type") == "AccountUpdated": if payload["new_status"] == "paid": ... elif payload["new_status"] == "trial": ... else: print("Omg, what is this?!")
  27. def handle(payload: dict) -> None: # HAMMER TIME! 🔨 match

    payload: case {"type": "AccountCreated"} : ... case _: if payload.get("type") == "AccountDeleted": ... elif payload.get("type") == "AccountUpdated": if payload["new_status"] == "paid": ... elif payload["new_status"] == "trial": ... else: print("Omg, what is this?!")
  28. def handle(payload: dict) -> None: # HAMMER TIME! 🔨 match

    payload: case {"type": "AccountCreated"} : ... case {"type": "AccountDeleted"} : ... case _: if payload.get("type") == "AccountUpdated": if payload["new_status"] == "paid": ... elif payload["new_status"] == "trial": ... else: print("Omg, what is this?!")
  29. def handle(payload: dict) -> None: # HAMMER TIME! 🔨 match

    payload: case {"type": "AccountCreated"} : ... case {"type": "AccountDeleted"} : ... case {"type": "AccountUpdated"} : if payload["new_status"] == "paid": ... elif payload["new_status"] == "trial": ... case _: print("Omg, what is this?!")
  30. def handle(payload: dict) -> None: # HAMMER TIME! 🔨 match

    payload: case {"type": "AccountCreated"} : ... case {"type": "AccountDeleted"} : ... case {"type": "AccountUpdated", "new_status": "paid"} : ... case {"type": "AccountUpdated", "new_status": "trial"} : ... case _: print("Omg, what is this?!")
  31. def handle(payload: dict) -> None: # HAMMER TIME! 🔨 match

    payload: case {"type": "AccountCreated"} : ... case {"type": "AccountDeleted"} : ... case {"type": "AccountUpdated", "new_status": "paid"} : ... case {"type": "AccountUpdated", "new_status": "trial"} : ... case _: print("Omg, what is this?!")
  32. Pydantic model data = {"type": "AccountCreated", "id": "64160ccb", "name": "Matthew"}

    class AccountCreated(BaseModel) : type: str id: str name: str AccountCreated( ** data) # AccountCreated(type='AccountCreated', id='64160ccb', name='Matthew')
  33. Pydantic model with a constant data = {"type": "AccountCreated", "id":

    "64160ccb", "name": "Matthew"} class AccountCreated(BaseModel) : type: Literal["AccountCreated"] id: str name: str AccountCreated(type="BAZINGA", id=„64160ccb", name="Matthew") # raises exception
  34. Pydantic models class AccountCreated(BaseModel) : ... class AccountDeleted(BaseModel) : ...

    class AccountUpdated(BaseModel) : type: Literal["AccountUpdated"] id: str old_status: Literal["trial", "paid"] new_status: Literal["trial", "paid"]
  35. def handle(payload: dict) -> None: match payload: case {"type": "AccountCreated"}

    : ... case {"type": "AccountDeleted"} : ... case {"type": "AccountUpdated", "new_status": "paid"} : ... case {"type": "AccountUpdated", "new_status": "trial"} : ... case _: print("Omg, what is this?!")
  36. def handle(payload: dict) -> None: match payload: case {"type": "AccountCreated"}

    : event = AccountCreated( ** payload) case {"type": "AccountDeleted"} : event = AccountDeleted( ** payload) case {"type": "AccountUpdated", "new_status": "paid"} : event = AccountUpdated( ** payload) case {"type": "AccountUpdated", "new_status": "trial"} : event = AccountUpdated( ** payload) case _: print("Omg, what is this?!”)
  37. def handle(payload: dict) -> None: supported_model = ( AccountCreated |

    AccountDeleted | AccountUpdated ) match payload: case {"type": "AccountCreated"} : ...
  38. def handle(payload: dict) -> None: supported_model = ( AccountCreated |

    AccountDeleted | AccountUpdated ) adapter = TypeAdapter(supported_model) match payload: case {"type": "AccountCreated"} : ...
  39. def handle(payload: dict) -> None: supported_model = ( AccountCreated |

    AccountDeleted | AccountUpdated ) adapter = TypeAdapter(supported_model) event = adapter.validate_python(payload) # event will be an instance of a supported model! match payload: case {"type": "AccountCreated"} : ...
  40. def handle(payload: dict) -> None: supported_model = ( AccountCreated |

    AccountDeleted | AccountUpdated ) adapter = TypeAdapter(supported_model) event = adapter.validate_python(payload) match event: case AccountCreated() : ... case AccountUpdated(new_status="paid") : ... case AccountUpdated(new_status="trial") : ...
  41. def handle(payload: dict) -> None: supported_model = ( AccountCreated |

    AccountDeleted | AccountUpdated | Any ) adapter = TypeAdapter(supported_model) event = adapter.validate_python(payload) match event: case AccountCreated() : ... case AccountUpdated(new_status="paid") : ... case AccountUpdated(new_status="trial") : ... case _: print("Omg, what is this?!")
  42. def handle(payload: dict) -> None: supported_model = ( AccountCreated |

    AccountDeleted | AccountUpdated | Any ) adapter = TypeAdapter(supported_model) event = adapter.validate_python(payload) match event: case AccountCreated() : ... case AccountUpdated(new_status="paid") : ... case AccountUpdated(new_status="trial") : ... case _: print("Omg, what is this?!")
  43. def handle(payload: dict) -> None: supported_model = ( AccountCreated |

    AccountDeleted | AccountUpdated | Any ) adapter = TypeAdapter(supported_model) event = adapter.validate_python(payload) match event: case AccountCreated() : ... case AccountUpdated(new_status="paid") : ... case AccountUpdated(new_status="trial") : ... case _: print("Omg, what is this?!")
  44. class AccountUpdatedToPaid(BaseModel) : type: Literal["AccountUpdated"] id: str old_status: Literal["trial"] new_status:

    Literal["paid"] class AccountUpdatedToTrial(BaseModel) : type: Literal["AccountUpdated"] id: str old_status: Literal["paid"] new_status: Literal["trial"]
  45. def handle(payload: dict) -> None: supported_model = ( ... |

    Any ) adapter = TypeAdapter(supported_model) event = adapter.validate_python(payload) match event: case AccountCreated() : ... case AccountUpdatedToPaid() : ... case AccountUpdatedToTrial() : ... case _: print("Omg, what is this?!")
  46. import functools @functools.singledispatch def event_handler(event: Any) -> None: print("Omg, what

    is this?!") @event_handler.register def handle_account_created(event: AccountCreated) -> None: ... @event_handler.register def handle_account_updated_to_paid(event: AccountUpdatedToPaid) -> None: ...
  47. ef handle(payload: dict) -> None: supported_model = ( AccountCreated |

    AccountDeleted | AccountUpdatedToTrial | AccountUpdatedToPaid | Any ) adapter = TypeAdapter(supported_model) event = adapter.validate_python(payload) event_handler(event)
  48. match latte: case Coffee() : print("It's caffee latte or latte

    machiato!") case Tea() : print("It's Matcha Latte") case _: print("Huh, no idea what is it") match…case on types
  49. match event: case AccountCreated() : ... case AccountUpdated(new_status="paid") : ...

    case AccountUpdated(new_status="trial") : ... case _: print("Omg, what is this?!") match…case on attributes' values
  50. pattern matching using Pydantic class AccountCreated(BaseModel) : type: Literal["AccountCreated"] id:

    str name: str class AccountDeleted(BaseModel) : type: Literal["AccountDeleted"] id: str supported_model = ( AccountCreated | AccountDeleted | AccountUpdated ) adapter = TypeAdapter(supported_model) event = adapter.validate_python(payload)
  51. pattern matching using Pydantic class AccountCreated(BaseModel) : type: Literal["AccountCreated"] id:

    str name: str class AccountDeleted(BaseModel) : type: Literal["AccountDeleted"] id: str supported_model = ( AccountCreated | AccountDeleted | AccountUpdated | Any ) adapter = TypeAdapter(supported_model) event = adapter.validate_python(payload)
  52. Dispatch based on type using @singledispatch import functools @functools.singledispatch def

    event_handler(event: Any) -> None: print("Omg, what is this?!") @event_handler.register def handle_account_created(event: AccountCreated) -> None: ...
  53. Bonus Performance comparison • ifs are faster than match on

    dicts 5.22 times • match on dicts is faster than match on pydantic models 5.23 times • ifs are faster than match on pydantic models 27.28 times • ifs are faster than match on pydantic models with discriminant union 16.25 times Absolute times are: ifs: 2.0783760119229556e-07s match_dicts: 1.0846141958609223e-06s match_pydantic: 5.670525901950896e-06s match_pydantic_optimized: 3.377836011350155e-06s https://github.com/Enforcer/match-case-talk/blob/main/benchmark.py