Package: awesome-pattern-matching
Awesome Pattern Matching (apm) for Pythonpipinstallawesome-pattern-matchingSimplePowerfulExtensibleComposableFunctionalPython 3.7+, PyPy3.7+Typed (IDE friendly)Offers different styles (expression, declarative, statement, ...)There's a ton of pattern matching libraries available for python, all with varying degrees of maintenance and usability;
alsosince Python 3.10 there is the PEP-634matchstatement. However,
this library still offers functionality that PEP-634 doesn't offer, as well as pattern matching for python versions
before 3.10.A detailed comparison of PEP-634 andapmis available.apmdefines patterns as objects which arecomposableandreusable. Pieces can be matched and captured into
variables, much like pattern matching in Haskell or Scala (a feature which most libraries actually lack, but which also
makes pattern matching useful in the first place - the capability to easily extract data). Here is an example:fromapmimport*ifresult:=match([1,2,3,4,5],[1,'2nd'@_,'3rd'@_,'tail'@Remaining(...)]):print(result['2nd'])# 2print(result['3rd'])# 3print(result['tail'])# [4, 5]# If you find it more readable, '>>' can be used instead of '@' to capture a variablematch([1,2,3,4,5],[1,_>>'2nd',_>>'3rd',Remaining(...)>>'tail'])Patterns can be composed using&,|, and^, or via their more explicit counterpartsAllOf,OneOf, andEither. Since patterns are objects, they can be stored in variables and be reused.positive_integer=InstanceOf(int)&Check(lambdax:x>=0)Some fancy matching patterns are available out of the box:fromapmimport*deff(x:int,y:float)->int:passifmatch(f,Arguments(int,float)&Returns(int)):print("Function satisfies required signature")Multiple StylesFor matching and selecting from multiple cases, choose your style:fromapmimport*value=7# The simple styleifmatch(value,Between(1,10)):print("It's between 1 and 10")elifmatch(value,Between(11,20)):print("It's between 11 and 20")else:print("It's not between 1 and 20")# The expression stylecase(value)\.of(Between(1,10),lambda:print("It's between 1 and 10"))\.of(Between(11,20),lambda:print("It's between 11 and 20"))\.otherwise(lambda:print("It's not between 1 and 20"))# The statement styletry:match(value)exceptCase(Between(1,10)):print("It's between 1 and 10")exceptCase(Between(11,20)):print("It's between 11 and 20")exceptDefault:print("It's not between 1 and 20")# The declarative style@case_distinctiondeff(n:Match(Between(1,10))):print("It's between 1 and 10")@case_distinctiondeff(n:Match(Between(11,20))):print("It's between 11 and 20")@case_distinctiondeff(n):print("It's not between 1 and 20")f(value)# The terse (pampy) stylematch(value,Between(1,10),lambda:print("It's between 1 and 10"),Between(11,20),lambda:print("It's between 11 and 20"),_,lambda:print("It's not between 1 and 20"))Nested pattern matchesPatterns are applied recursively, such that nested structures can be matched arbitrarily deep.
This is super useful for extracting data from complicated structures:fromapmimport*sample_k8s_response={"containers":[{"args":["--cert-dir=/tmp","--secure-port=4443","--kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname","--kubelet-use-node-status-port"],"image":"k8s.gcr.io/metrics-server/metrics-server:v0.4.1","imagePullPolicy":"IfNotPresent","name":"metrics-server","ports":[{"containerPort":4443,"name":"https","protocol":"TCP"}]}]}ifresult:=match(sample_k8s_response,{"containers":Each({"image":'image'@_,"name":'name'@_,"ports":Each({"containerPort":'port'@_}),})}):print(f"Image:{result['image']}, Name:{result['name']}, Port:{result['port']}")The above will printImage: k8s.gcr.io/metrics-server/metrics-server:v0.4.1, Name: metrics-server, Port: 4443MultimatchBy defaultmatchrecords only the last match for captures. If for example'item' @ InstanceOf(int)matches multiple times,
the last match will be recorded inresult['item'].matchcan record all captures using themultimatch=Trueflag:ifresult:=match([{'foo':5},3,{'foo':7,'bar':9}],Each(OneOf({'foo':'item'@_},...)),multimatch=True):print(result['item'])# [5, 7]# The default since v0.15.0 is multimatch=Falseifresult:=match([{'foo':5},3,{'foo':7,'bar':9}],Each(OneOf({'foo':'item'@_},...))):print(result['item'])# 7Strict vs non-strict matchesAny value which occurs verbatim in a pattern is matched verbatim (int,str,list, ...), except Dictionaries (
anything which has anitems()actually).Thus:some_very_complex_object={"A":1,"B":2,"C":3,}match(some_very_complex_object,{"C":3})# matches!If you do not want unknown keys to be ignored, wrap the pattern in aStrict:# does not match, only matches exactly `{"C": 3}`match(some_very_complex_object,Strict({"C":3}))Lists (anything iterable which does not have anitems()actually) are also compared as they are, i.e.:ls=[1,2,3]match(ls,[1,2,3])# matchesmatch(ls,[1,2])# does not matchMatch head and tail of a listIt is possible to match the remainder of a list though:match(ls,[1,2,Remaining(InstanceOf(int))])And each item:match(ls,Each(InstanceOf(int)))Patterns can be joined using&,|, and^:match(ls,Each(InstanceOf(int)&Between(1,3)))Wild-card matches are supported using Ellipsis (...):match(ls,[1,Remaining(...,at_least=2)])The above example also showcases howRemainingcan be made to matchat_leastnnumber of items (Eachalso has anat_leastkeyword argument).Wildcard matches anything using_A wildcard pattern can be expressed using_._is aPatternand thus>>and@can be used with it.match([1,2,3,4],[1,_,3,_])Wildcard matches anything using...TheEllipsiscan be used as a wildcard match, too. It is however not aPattern(so|,&,@, etc. can not
be used on it). If you actually want to matchEllipsis, wrap it usingValue(...).Otherwise...is equivalent for most intents and purposes to_:match([1,2,3,4],[1,...,3,...])Support for dataclasses@dataclassclassUser:first_name:strlast_name:strvalue=User("Jane","Doe")ifmatch(value,User(_,"Doe")):print("Welcome, member of the Doe family!")elifmatch(value,User(_,_)):print("Welcome, anyone!")The different styles in detailSimple style💚 has access to result captures💚 vanilla python💔 no case guards💔 can not return values (since it's a statement, not an expression)🖤 a bit repetetive💚 simplest and most easy to understand style🖤 fastest of them allfromapmimport*value={"a":7,"b":"foo","c":"bar"}ifresult:=match(value,EachItem(_,'value'@InstanceOf(str)|...),multimatch=True):print(result['value'])# ["foo", "bar"]pre:=version (Python 3.7)bind()can be used on aMatchResultto bind the matched items to an existing dictionary.fromapmimport*value={"a":7,"b":"foo","c":"bar"}result={}ifmatch(value,EachItem(_,'value'@InstanceOf(str)|...)).bind(result):print(result['value'])# ["foo", "bar"]elifmatch(value,{"quux":_>>'quux'}).bind(result):print(result['quux'])Expression style💚 has access to result captures💚 vanilla python💚 can return values directly as it is an expression💚 can use case guards viawhen=orguarded🖤 so terse that it is sometimes hard to readThe expression style is summarized:case(value).of(pattern,action)....otherwise(default_action)...where action is either a value or a callable. The captures from the matching result are bound to the named
parameters of the given callable, i.e.result['foo']andresult['bar']from'foo' @ _and'bar' @ _will be
bound tofooandbarrespectively inlambda foo, bar: ....fromapmimport*display_name=case({'user':'some-user-id','first_name':"Jane",'last_name':"Doe"})\.of({'first_name':'first'@_,'last_name':'last'@_},lambdafirst,last:f"{first},{last}")\.of({'user':'user_id'@_},lambdauser_id:f"#{user_id}")\.otherwise("anonymous")Note: To return a value an.otherwise(...)case must always be present.Statement styleThis is arguable the most hacky style inapm, as it re-uses thetry .. exceptmechanism. It is nevertheless quite readable.💚 has access to result captures💚 very readable💔 can not return values (since it's a statement, not an expression)💚 can use case guards viawhen=🖤 misuse of thetry .. exceptstatementfromapmimport*try:match({'user':'some-user-id','first_name':"Jane",'last_name':"Doe"})exceptCase({'first_name':'first'@_,'last_name':'last'@_})asresult:user=f"{result['first']}{result['last']}"exceptCase({'user':'user_id'@_})asresult:user=f"#{result['user_id']}"exceptDefault:user="anonymous"print(user)# "Jane Doe"Declarative style💔 does not have access to result captures💚 very readable💚 can use case guards viawhen=💚 can return values🖤 the most bloated version of all stylesfromapmimport*@case_distinctiondeffib(n:Match(OneOf(0,1))):returnn@case_distinctiondeffib(n):returnfib(n-2)+fib(n-1)foriinrange(0,6):print(fib(i))Nota bene: Overloading using@case_distinctionIf not for its pattern matching capabilities,@case_distinctioncan be used
to implement overloading. In fact, it can be imported as@overload.
The mechanism is aware of arity and argument types.fromapm.overloadimportoverload@overloaddefadd(a:str,b:str):return"".join([a,b])@overloaddefadd(a:int,b:int):returna+badd("a","b")add(1,2)Terse style💚 has access to result captures💚 can use case guards viaguarded💚 very concise💚 can return values🖤 very readable when formatted nicely🖤 not so well suited for larger match actions🖤 slowest of them allAs the name indicates the "terse" style is terse. It is inspired by thepampypattern matching library and mimics some of its behavior. Despite a slim surface
area it also comes with some simplifications:A type given as a pattern is matched against as if it was wrapped in anInstanceOfre.Patternobjects (result ofre.compile) are matched against as if it was given viaRegexCaptures are passed to actions in the same order as they occur in the pattern (not by name)fromapmimport*deffibonacci(n):returnmatch(n,1,1,2,1,_,lambdax:fibonacci(x-1)+fibonacci(x-2))fibonacci(6)# -> 8classAnimal:passclassHippo(Animal):passclassZebra(Animal):passclassHorse(Animal):passdefwhat_am_i(x):returnmatch(x,Hippo,'hippopotamus',Zebra,'zebra',Animal,'some other animal',_,'not at all an animal',)what_am_i(Hippo())# -> 'hippopotamus'what_am_i(Zebra())# -> 'zebra'what_am_i(Horse())# -> 'some other animal'what_am_i(42)# -> 'not at all an animal'Available patternsCapture(pattern, name=<str>)Captures a piece of the thing being matched by name.ifresult:=match([1,2,3,4],[1,2,Capture(Remaining(InstanceOf(int)),name='tail')]):print(result['tail'])## -> [3, 4]As this syntax is rather verbose, two shorthand notations can be used:# using the matrix multiplication operator '@' (syntax resembles that of Haskell and Scala)ifresult:=match([1,2,3,4],[1,2,'tail'@Remaining(InstanceOf(int))]):print(result['tail'])## -> [3, 4]# using the right shift operatorifresult:=match([1,2,3,4],[1,2,Remaining(InstanceOf(int))>>'tail']):print(result['tail'])## -> [3, 4]Strict(pattern)Performs a strict pattern match. A strict pattern match also compares the type of verbatim values. That is, whileapmwould match3with3.0it would not do so when usingStrict. Alsoapmperforms partial matches of
dictionaries (that is: it ignores unknown keys). It will perform an exact match for dictionaries usingStrict.# The following will matchmatch({"a":3,"b":7},{"a":...})match(3.0,3)# These will not matchmatch({"a":3,"b":7},Strict({"a":...}))match(3.0,Strict(3))OneOf(*pattern)Matches against any of the provided patterns. Equivalent top1 | p2 | p3 | ..(but operator overloading does not work with values that do not inherit fromPattern)match("quux",OneOf("bar","baz","quux"))match(3,OneOf(InstanceOf(int),None))Patterns can also be joined using|to form aOneOfpattern:match(3,InstanceOf(int)|InstanceOf(float))The above example is rather contrived, asInstanceOfalready accepts multiple types natively:match(3,InstanceOf(int,float))Since bare values do not inherit fromPatternthey can be wrapped inValue:match("quux",Value("foo")|Value("quux"))AllOf(*pattern)Checks whether the value matches all of the given pattern. Equivalent top1 & p2 & p3 & ..(but operator overloading does not work with values that do not inherit fromPattern)match("quux",AllOf(InstanceOf("str"),Regex("[a-z]+")))NoneOf(*pattern)Same asNot(OneOf(*pattern))(also~OneOf(*pattern)).Not(pattern)Matches if the given pattern does not match.match(3,Not(4))# matchesmatch(5,Not(4))# matchesmatch(4,Not(4))# does not matchThe bitflip prefix operator (~) can be used to express the same thing. Note that it does not work on bare values,
so they need to be wrapped inValue.match(3,~Value(4))# matchesmatch(5,~Value(4))# matchesmatch(4,~Value(4))# does not matchNotcan be used do create aNoneOfkind of pattern:match("string",~OneOf("foo","bar"))# matches everything except "foo" and "bar"Notcan be used to create a pattern that never matches:Not(...)Each(pattern [, at_least=]Matches each item in an iterable.match(range(1,10),Each(Between(1,9)))EachItem(key_pattern, value_pattern)Matches an object if each key satisfieskey_patternand each value satisfiesvalue_pattern.match({"a":1,"b":2},EachItem(Regex("[a-z]+"),InstanceOf(int)))Some(pattern)(akaManyandRemaining)Matches a sequence of items within a list:ifresult:=match(range(1,10),[1,'a'@Some(...),4,'b'@Some(...),8,9]):print(result['a'])# [2, 3]print(result['b'])# [5, 6, 7]Takes the optional valuesexactly,at_least, andat_mostwhich makesSomematch
eitherexactlynitems,at_leastn, orat_mostnitems (at_leastandat_mostcan be given at the same
time, but not together withexactly).Note the difference betweenSome(1, 2)andSome([1, 2]). The first version matches subsequences, the second
version matches items which are themselves lists:match([0,1,2,1,2,3],[0,Some(1,2),3])# matches the subsequence 1, 2 twicematch([0,[1,2],[1,2],3],[0,Some([1,2]),3])# matches the item [1, 2] twice, which happen to be listsSomealso goes by the names ofManyandRemaining, which is sometimes nice to convey meaning:match(range(1,10),[1,2,'remaining'@Remaining()])match([0,1,1,1,2,1],[0,Many(1),Remaining(InstanceOf(int))])When used with no arguments,Some()is the same asSome(...).Remainder(pattern)Can be used to match the unmatched parts of a Dictionary/Mapping.result=match({"foo":1,"bar":2,"qux":4,"quuz":8,},{"foo":'foo'@_,"bar":'bar'@_}**Remainder('rs'@_))print(result.foo)# 1print(result.bar)# 2print(result.rs)# {'qux': 4, 'quuz': 8}Remainderis, strictly speaking, not aPatternand only works in conjunction with**on dictionaries,
and it only works on the right-hand side of the dictionary.Between(lower, upper)Matches an object if it is betweenlowerandupper(inclusive). The optional keyword argumentslower_bound_exclusiveandupper_bound_exclusivecan be set toTruerespectively to exclude the
lower/upper from the range of matching values.Length(length)Matches an object if it has the given length. Alternatively also acceptsat_leastandat_mostkeyword arguments.match("abc",Length(3))match("abc",Length(at_least=2))match("abc",Length(at_most=4))match("abc",Length(at_least=2,at_most=4))Contains(item)Matches an object if it contains the given item (as per the same logic as theinoperator).match("hello there, world",Contains("there"))match([1,2,3],Contains(2)&Contains(3))match({'foo':1,'bar':2},Contains('quux')|Contains('bar'))Regex(regex_pattern, bind_groups: bool = True)Matches a string if it completely matches the given regex, as perre.fullmatch.
If the regular expression pattern contains named capturing groups andbind_groupsis set toTrue,
this pattern will bind the captured results in theMatchResult(the default).To mimicre.matchorre.searchthe given regular expressionxcan be augmented asx.*or.*x.*respectively.Check(predicate)Matches an object if it satisfies the given predicate.match(2,Check(lambdax:x%2==0))InstanceOf(*types)Matches an object if it is an instance of any of the given types.match(1,InstanceOf(int,flaot))SubclassOf(*types)Matches if the matched type is a subclass of any of the given types.match(int,SubclassOf(int,float))Parameters(...)Matches the parameters of a callable.deff(x:int,*xs:float,y:str,**kwargs:bool):passmatch(f,Parameters(int,VarArgs(float),y=str,KwArgs(bool)))Each argument to Parameters is expected to be the type of a positional argument.Parametersmatches function signatures if their positional arguments match completely, i.e.deff(x:int,y:float):passprint(bool(match(f,Parameters(int))))# Falseprint(bool(match(f,Parameters(int,float))))# Trueprint(bool(match(f,Parameters(int,Remaining(_)))))# TrueKeyword arguments are matched only if they are keyword only arguments. In contrast to positional arguments it matches
also impartially (which aligns with the non-strict matching behavior with respect to dictionaries):deff(x:int,*,y:str,z:float):passprint(bool(match(f,Parameters(int))))# Trueprint(bool(match(f,Parameters(y=str))))# False – positional parameters not matchedprint(bool(match(f,Parameters(int,y=str))))# TrueThis can be changed withStrict:deff(x:int,*,y:str,z:float):passprint(bool(match(f,Strict(Parameters(int)))))# Falseprint(bool(match(f,Strict(Parameters(int,y=str)))))# False  (z not mentioned but present)print(bool(match(f,Strict(Parameters(int,y=str,z=float)))))# True  (has y and z exactly)Arguments(*types)DEPRECATED, useParametersinstead (see above)Matches a callable if it's type annotations correspond to the given types.deff(x:int,y:float,z):...match(f,Arguments(int,float,None))Arguments has an alternate form which can be used to match keyword arguments:deff(x:int,y:float,z:str):...match(f,Arguments(x=int,y=float))The strictness rules are the same as for dictionaries (which is why the above example works).# given the f from abovematch(f,Strict(Arguments(x=int,y=float)))# does not matchmatch(f,Strict(Arguments(x=int,y=float,z=str)))# matchesReturns(type)Matches a callable if it's type annotations denote the given return type.defg(x:int)->str:...match(g,Arguments(int)&Returns(str))Transformed(function, pattern)Transforms the currently looked at value by applyingfunctionon it and matches the result againstpattern. In
Haskell and other languages this is known as aview
pattern.defsha256(v:str)->str:importhashlibreturnhashlib.new('sha256',v.encode('utf8')).hexdigest()match("hello",Transformed(sha256,"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"))This is handy for matching data types likedatetime.dateas this pattern won't match if the transformation
function errored out with an exception.fromapmimport*fromdatetimeimportdateifresult:=match("2020-08-27",Transformed(date.fromisoformat,'date'@_):print(repr(result['date']))# result['date'] is a datetime.dateAt(path, pattern)Checks whether the nested object to be matched satisfies pattern at the given path. The match fails if the given path
can not be resolved.record={"foo":{"bar":{"quux":{"value":"deeply nested"}}}}result:=match(record,At("foo.bar.quux",{"value":Capture(...,name="value")}))result['value']# "deeply nested"# alternate formresult:=match(record,At(['foo','bar','quux'],{"value":Capture(...,name="value")}))Items(**kwargs))Mostly syntactic sugar to match a dictionary nicely (and anything that provides an.items()method).fromapmimport*fromdatetimeimportdatetimerequest={"api_version":"v1","job":{"run_at":"2020-08-27 14:09:30","command":"echo 'booya'",}}ifresult:=match(request,Items(api_version="v1",job=Object(run_at=Transformed(datetime.fromisoformat,'time'@_),)&OneOf(Items(command='command'@InstanceOf(str)),Items(spawn='container'@InstanceOf(str)),))):print(repr(result['time']))# datetime(2020, 8, 27, 14, 9, 30)print('container'notinresult)# Trueprint(result['command'])# "echo 'booya'"Object(type, *args, **kwargs)Matches any object of the specific type with the given attrs as in**kwargs.
It respects the__match_args__introduced by PEP-634.fromapmimport*fromtypingimportLiteral,TupleclassClick:__match_args__=("position","button")def__init__(self,pos:Tuple[int,int],btn:Literal['left','right','middle']):self.position=posself.button=btnassertmatch(Click((1,2),'left'),Object(Click,(1,2)))assertmatch(Click((1,2),'left'),Object(Click,(1,2),'left'))assertmatch(Click((1,2),'left'),Object(Click,(1,2),button='left'))ExtensibleNew patterns can be added, just like the ones inapm.patterns.*. Simply extend theapm.Patternclass:classMin(Pattern):def__init__(self,min):self.min=mindefmatch(self,value,*,ctx:MatchContext,strict=False)->MatchResult:returnctx.match_if(value>=self.min)match(3,Min(1))# matchesmatch(3,Min(5))# does not match