Pampy is pretty small (150 lines), reasonably fast, and often makes your code more readable and hence easier to reason about. There is also a JavaScript version, called Pampy.js.
Patterns are evaluated in the order they appear.
The operator _ means "any other case I didn't think of".
from pampy import match, _def fibonacci(n):return match(n,1, 1,2, 1,_, lambda x: fibonacci(x-1) + fibonacci(x-2) )
from pampy import match, REST, _def lisp(exp):return match(exp,int, lambda x: x,callable, lambda x: x, (callable, REST), lambda f, rest: f(*map(lisp, rest)),tuple, lambda t: list(map(lisp, t)), )plus = lambda a, b: a + bminus = lambda a, b: a - bfrom functools import reducelisp((plus, 1, 2)) # => 3lisp((plus, 1, (minus, 4, 2))) # => 3lisp((reduce, plus, (range, 10))) # => 45
match(x,3, "this matches the number 3",int, "matches any integer", (str, int), lambda a, b: "a tuple (a, b) you can use in a function", [1, 2, _], "any list of 3 elements that begins with [1, 2]", {'x': _}, "any dict with a key 'x' and any value associated",_, "anything else")
from pampy import match, HEAD, TAIL, _x = [1, 2, 3]match(x, [1, TAIL], lambda t: t) # => [2, 3]match(x, [HEAD, TAIL], lambda h, t: (h, t)) # => (1, [2, 3])
TAIL
and REST
actually mean the same thing.
from pampy import match, _x = [1, [2, 3], 4]match(x, [1, [_, 3], _], lambda a, b: [1, [a, 3], b]) # => [1, [2, 3], 4]
pet = { 'type': 'dog', 'details': { 'age': 3 } }match(pet, { 'details': { 'age': _ } }, lambda age: age) # => 3match(pet, { _ : { 'age': _ } }, lambda a, b: (a, b)) # => ('details', 3)
It feels like putting multiple _ inside dicts shouldn't work. Isn't ordering in dicts not guaranteed ? But it does because in Python 3.7, dict maintains insertion key order by default
class Pet: passclass Dog(Pet): passclass Cat(Pet): passclass Hamster(Pet): passdef what_is(x):return match(x,Dog, 'dog',Cat, 'cat',Pet, 'any other pet', _, 'this is not a pet at all', )what_is(Cat()) # => 'cat'what_is(Dog()) # => 'dog'what_is(Hamster()) # => 'any other pet'what_is(Pet()) # => 'any other pet'what_is(42) # => 'this is not a pet at all'
Pampy supports Python 3.7 dataclasses. You can pass the operator _
as arguments and it will match those fields.
@dataclassclass Pet:name: strage: intpet = Pet('rover', 7)match(pet, Pet('rover', _), lambda age: age) # => 7match(pet, Pet(_, 7), lambda name: name) # => 'rover'match(pet, Pet(_, _), lambda name, age: (name, age)) # => ('rover', 7)
Pampy supports typing annotations.
class Pet: passclass Dog(Pet): passclass Cat(Pet): passclass Hamster(Pet): passtimestamp = NewType("year", Union[int, float])def annotated(a: Tuple[int, float], b: str, c: E) -> timestamp:passmatch((1, 2), Tuple[int, int], lambda a, b: (a, b)) # => (1, 2)match(1, Union[str, int], lambda x: x) # => 1match('a', Union[str, int], lambda x: x) # => 'a'match('a', Optional[str], lambda x: x) # => 'a'match(None, Optional[str], lambda x: x) # => Nonematch(Pet, Type[Pet], lambda x: x) # => Petmatch(Cat, Type[Pet], lambda x: x) # => Catmatch(Dog, Any, lambda x: x) # => Dogmatch(Dog, Type[Any], lambda x: x) # => Dogmatch(15, timestamp, lambda x: x) # => 15match(10.0, timestamp, lambda x: x) # => 10.0match([1, 2, 3], List[int], lambda x: x) # => [1, 2, 3]match({'a': 1, 'b': 2}, Dict[str, int], lambda x: x) # => {'a': 1, 'b': 2}match(annotated, Callable[[Tuple[int, float], str, Pet], timestamp], lambda x: x) # => annotated
For iterable generics actual type of value is guessed based on the first element.
match([1, 2, 3], List[int], lambda x: x) # => [1, 2, 3]match([1, "b", "a"], List[int], lambda x: x) # => [1, "b", "a"]match(["a", "b", "c"], List[int], lambda x: x) # raises MatchErrormatch(["a", "b", "c"], List[Union[str, int]], lambda x: x) # ["a", "b", "c"]match({"a": 1, "b": 2}, Dict[str, int], lambda x: x) # {"a": 1, "b": 2}match({"a": 1, "b": "dog"}, Dict[str, int], lambda x: x) # {"a": 1, "b": "dog"}match({"a": 1, 1: 2}, Dict[str, int], lambda x: x) # {"a": 1, 1: 2}match({2: 1, 1: 2}, Dict[str, int], lambda x: x) # raises MatchErrormatch({2: 1, 1: 2}, Dict[Union[str, int], int], lambda x: x) # {2: 1, 1: 2}
Iterable generics also match with any of their subtypes.
match([1, 2, 3], Iterable[int], lambda x: x) # => [1, 2, 3]match({1, 2, 3}, Iterable[int], lambda x: x) # => {1, 2, 3}match(range(10), Iterable[int], lambda x: x) # => range(10)match([1, 2, 3], List[int], lambda x: x) # => [1, 2, 3]match({1, 2, 3}, List[int], lambda x: x) # => raises MatchErrormatch(range(10), List[int], lambda x: x) # => raises MatchErrormatch([1, 2, 3], Set[int], lambda x: x) # => raises MatchErrormatch({1, 2, 3}, Set[int], lambda x: x) # => {1, 2, 3}match(range(10), Set[int], lambda x: x) # => raises MatchError
For Callable any arg without annotation treated as Any.
def annotated(a: int, b: int) -> float:passdef not_annotated(a, b):passdef partially_annotated(a, b: float):passmatch(annotated, Callable[[int, int], float], lambda x: x) # => annotatedmatch(not_annotated, Callable[[int, int], float], lambda x: x) # => raises MatchErrormatch(not_annotated, Callable[[Any, Any], Any], lambda x: x) # => not_annotatedmatch(annotated, Callable[[Any, Any], Any], lambda x: x) # => raises MatchErrormatch(partially_annotated, Callable[[Any, float], Any], lambda x: x) # => partially_annotated
TypeVar is not supported.
As Pattern you can use any Python type, any class, or any Python value.
The operator _
and built-in types like int
or str
, extract variables that are passed to functions.
Types and Classes are matched via instanceof(value, pattern)
.
Iterable
Patterns match recursively through all their elements. The same goes for dictionaries.
Pattern Example | What it means | Matched Example | Arguments Passed to function | NOT Matched Example |
---|---|---|---|---|
"hello" | only the string "hello" matches | "hello" | nothing | any other value |
None | only None | None | nothing | any other value |
int | Any integer | 42 | 42 | any other value |
float | Any float number | 2.35 | 2.35 | any other value |
str | Any string | "hello" | "hello" | any other value |
tuple | Any tuple | (1, 2) | (1, 2) | any other value |
list | Any list | [1, 2] | [1, 2] | any other value |
MyClass | Any instance of MyClass. And any object that extends MyClass. | MyClass() | that instance | any other object |
_ | Any object (even None) | that value | ||
ANY | The same as _ | that value | ||
(int, int) | A tuple made of any two integers | (1, 2) | 1 and 2 | (True, False) |
[1, 2, _] | A list that starts with 1, 2 and ends with any value | [1, 2, 3] | 3 | [1, 2, 3, 4] |
[1, 2, TAIL] | A list that start with 1, 2 and ends with any sequence | [1, 2, 3, 4] | [3, 4] | [1, 7, 7, 7] |
{'type':'dog', age: _ } | Any dict with type: "dog" and with an age | {"type":"dog", "age": 3} | 3 | {"type":"cat", "age":2} |
{'type':'dog', age: int } | Any dict with type: "dog" and with an int age | {"type":"dog", "age": 3} | 3 | {"type":"dog", "age":2.3} |
re.compile('(w+)-(w+)-cat$') | Any string that matches that regular expression expr | "my-fuffy-cat" | "my" and "puffy" | "fuffy-dog" |
Pet(name=_, age=7) | Any Pet dataclass with age == 7 | Pet('rover', 7) | ['rover'] | Pet('rover', 8) |
Any | The same as _ | that value | ||
Union[int, float, None] | Any integer or float number or None | 2.35 | 2.35 | any other value |
Optional[int] | The same as Union[int, None] | 2 | 2 | any other value |
Type[MyClass] | Any subclass of MyClass. And any class that extends MyClass. | MyClass | that class | any other object |
Callable[[int], float] | Any callable with exactly that signature | def a(q:int) -> float: ... | that function | def a(q) -> float: ... |
Tuple[MyClass, int, float] | The same as (MyClass, int, float) | |||
Mapping[str, int] Any subtype of Mapping acceptable too | any mapping or subtype of mapping with string keys and integer values | {'a': 2, 'b': 3} | that dict | {'a': 'b', 'b': 'c'} |
Iterable[int] Any subtype of Iterable acceptable too | any iterable or subtype of iterable with integer values | range(10) and [1, 2, 3] | that iterable | ['a', 'b', 'v'] |
By default match()
is strict. If no pattern matches, it raises a MatchError
.
You can instead provide a fallback value using default
to be used when nothing matches.
>>> match([1, 2], [1, 2, 3], "whatever") MatchError: '_' not provided. This case is not handled: [1, 2] >>> match([1, 2], [1, 2, 3], "whatever", default=False) False
Pampy supports Python's Regex. You can pass a compiled regex as pattern, and Pampy is going to run pattern.search()
, and then pass to the action function the result of .groups()
.
def what_is(pet):return match(pet,re.compile('(w+)-(w+)-cat$'), lambda name, my: 'cat '+name,re.compile('(w+)-(w+)-dog$'), lambda name, my: 'dog '+name,_, "something else")what_is('fuffy-my-dog') # => 'dog fuffy'what_is('puffy-her-dog') # => 'dog puffy'what_is('carla-your-cat') # => 'cat carla'what_is('roger-my-hamster') # => 'something else'
Pampy works in Python >= 3.6 Because dict matching can work only in the latest Pythons.
To install it:
$ pip install pampy
or$ pip3 install pampy
Pampy is Python3-first, but you can use most of its features in Python2 via this backport by Manuel Barkhau:
pip install backports.pampy
from backports.pampy import match, HEAD, TAIL, _