Package: pampy
Pampy: Pattern Matching for PythonPampy 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.You can write many patternsPatterns are evaluated in the order they appear.You can write FibonacciThe operator _ means "any other case I didn't think of".frompampyimportmatch,_deffibonacci(n):returnmatch(n,1,1,2,1,_,lambdax:fibonacci(x-1)+fibonacci(x-2))You can write a Lisp calculator in 5 linesfrompampyimportmatch,REST,_deflisp(exp):returnmatch(exp,int,lambdax:x,callable,lambdax:x,(callable,REST),lambdaf,rest:f(*map(lisp,rest)),tuple,lambdat:list(map(lisp,t)),)plus=lambdaa,b:a+bminus=lambdaa,b:a-bfromfunctoolsimportreducelisp((plus,1,2))# => 3lisp((plus,1,(minus,4,2)))# => 3lisp((reduce,plus,(range,10)))# => 45You can match so many things!match(x,3,"this matches the number 3",int,"matches any integer",(str,int),lambdaa,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")You can match [HEAD, TAIL]frompampyimportmatch,HEAD,TAIL,_x=[1,2,3]match(x,[1,TAIL],lambdat:t)# => [2, 3]match(x,[HEAD,TAIL],lambdah,t:(h,t))# => (1, [2, 3])TAILandRESTactually mean the same thing.You can nest lists and tuplesfrompampyimportmatch,_x=[1,[2,3],4]match(x,[1,[_,3],_],lambdaa,b:[1,[a,3],b])# => [1, [2, 3], 4]You can nest dicts. And you can use _ as key!pet={'type':'dog','details':{'age':3}}match(pet,{'details':{'age':_}},lambdaage:age)# => 3match(pet,{_:{'age':_}},lambdaa,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 becausein Python 3.7, dict maintains insertion key order by defaultYou can match class hierarchiesclassPet:passclassDog(Pet):passclassCat(Pet):passclassHamster(Pet):passdefwhat_is(x):returnmatch(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'Using DataclassesPampy supports Python 3.7 dataclasses. You can pass the operator_as arguments and it will match those fields.@dataclassclassPet:name:strage:intpet=Pet('rover',7)match(pet,Pet('rover',_),lambdaage:age)# => 7match(pet,Pet(_,7),lambdaname:name)# => 'rover'match(pet,Pet(_,_),lambdaname,age:(name,age))# => ('rover', 7)Using typingPampy supports typing annotations.classPet:passclassDog(Pet):passclassCat(Pet):passclassHamster(Pet):passtimestamp=NewType("year",Union[int,float])defannotated(a:Tuple[int,float],b:str,c:E)->timestamp:passmatch((1,2),Tuple[int,int],lambdaa,b:(a,b))# => (1, 2)match(1,Union[str,int],lambdax:x)# => 1match('a',Union[str,int],lambdax:x)# => 'a'match('a',Optional[str],lambdax:x)# => 'a'match(None,Optional[str],lambdax:x)# => Nonematch(Pet,Type[Pet],lambdax:x)# => Petmatch(Cat,Type[Pet],lambdax:x)# => Catmatch(Dog,Any,lambdax:x)# => Dogmatch(Dog,Type[Any],lambdax:x)# => Dogmatch(15,timestamp,lambdax:x)# => 15match(10.0,timestamp,lambdax:x)# => 10.0match([1,2,3],List[int],lambdax:x)# => [1, 2, 3]match({'a':1,'b':2},Dict[str,int],lambdax:x)# => {'a': 1, 'b': 2}match(annotated,Callable[[Tuple[int,float],str,Pet],timestamp],lambdax:x)# => annotatedFor iterable generics actual type of value is guessed based on the first element.match([1,2,3],List[int],lambdax:x)# => [1, 2, 3]match([1,"b","a"],List[int],lambdax:x)# => [1, "b", "a"]match(["a","b","c"],List[int],lambdax:x)# raises MatchErrormatch(["a","b","c"],List[Union[str,int]],lambdax:x)# ["a", "b", "c"]match({"a":1,"b":2},Dict[str,int],lambdax:x)# {"a": 1, "b": 2}match({"a":1,"b":"dog"},Dict[str,int],lambdax:x)# {"a": 1, "b": "dog"}match({"a":1,1:2},Dict[str,int],lambdax:x)# {"a": 1, 1: 2}match({2:1,1:2},Dict[str,int],lambdax:x)# raises MatchErrormatch({2:1,1:2},Dict[Union[str,int],int],lambdax:x)# {2: 1, 1: 2}Iterable generics also match with any of their subtypes.match([1,2,3],Iterable[int],lambdax:x)# => [1, 2, 3]match({1,2,3},Iterable[int],lambdax:x)# => {1, 2, 3}match(range(10),Iterable[int],lambdax:x)# => range(10)match([1,2,3],List[int],lambdax:x)# => [1, 2, 3]match({1,2,3},List[int],lambdax:x)# => raises MatchErrormatch(range(10),List[int],lambdax:x)# => raises MatchErrormatch([1,2,3],Set[int],lambdax:x)# => raises MatchErrormatch({1,2,3},Set[int],lambdax:x)# => {1, 2, 3}match(range(10),Set[int],lambdax:x)# => raises MatchErrorFor Callable any arg without annotation treated as Any.defannotated(a:int,b:int)->float:passdefnot_annotated(a,b):passdefpartially_annotated(a,b:float):passmatch(annotated,Callable[[int,int],float],lambdax:x)# => annotatedmatch(not_annotated,Callable[[int,int],float],lambdax:x)# => raises MatchErrormatch(not_annotated,Callable[[Any,Any],Any],lambdax:x)# => not_annotatedmatch(annotated,Callable[[Any,Any],Any],lambdax:x)# => raises MatchErrormatch(partially_annotated,Callable[[Any,float],Any],lambdax:x)# => partially_annotatedTypeVar is not supported.All the things you can matchAs Pattern you can use any Python type, any class, or any Python value.The operator_and built-in types likeintorstr, extract variables that are passed to functions.Types and Classes are matched viainstanceof(value, pattern).IterablePatterns match recursively through all their elements.  The same goes for dictionaries.Pattern ExampleWhat it meansMatched ExampleArguments Passed to functionNOT Matched Example"hello"only the string"hello"matches"hello"nothingany other valueNoneonlyNoneNonenothingany other valueintAny integer4242any other valuefloatAny float number2.352.35any other valuestrAny string"hello""hello"any other valuetupleAny tuple(1, 2)(1, 2)any other valuelistAny list[1, 2][1, 2]any other valueMyClassAny instance of MyClass.And any object that extends MyClass.MyClass()that instanceany other object_Any object (even None)that valueANYThe same as_that value(int, int)A tuple made of any two integers(1, 2)1and2(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 withtype: "dog"and with an age{"type":"dog", "age": 3}3{"type":"cat", "age":2}{'type':'dog', age: int }Any dict withtype: "dog"and with anintage{"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 withage == 7Pet('rover', 7)['rover']Pet('rover', 8)AnyThe same as_that valueUnion[int, float, None]Any integer or float number or None2.352.35any other valueOptional[int]The same asUnion[int, None]22any other valueType[MyClass]Any subclass of MyClass.And any class that extends MyClass.MyClassthat classany other objectCallable[[int], float]Any callable with exactly that signaturedef a(q:int) -> float: ...that functiondef a(q) -> float: ...Tuple[MyClass, int, float]The same as(MyClass, int, float)Mapping[str, int]Any subtype ofMappingacceptable tooany 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 ofIterableacceptable tooany iterable or subtype of iterable with integer valuesrange(10)and[1, 2, 3]that iterable['a', 'b', 'v']Using defaultBy defaultmatch()is strict. If no pattern matches, it raises aMatchError.You can instead provide a fallback value usingdefaultto 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)
FalseUsing Regular ExpressionsPampy supports Python's Regex. You can pass a compiled regex as pattern, and Pampy is going to runpatter.search(), and then pass to the action function the result of.groups().defwhat_is(pet):returnmatch(pet,re.compile('(\w+)-(\w+)-cat$'),lambdaname,my:'cat '+name,re.compile('(\w+)-(\w+)-dog$'),lambdaname,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'Install for Python3Pampy works in Python >= 3.6Because dict matching can work only in the latest Pythons.To install it:$ pip install pampyor$ pip3 install pampyIf you really must use Python2Pampy is Python3-first, but you can use most of its features in Python2 viathis backportby Manuel Barkhau:pip install backports.pampyfrombackports.pampyimportmatch,HEAD,TAIL,_