Package: pyPMatch
pyPMatchThis is work in progress!Expect some rough spots, and some features to still be missing.pyPMatchprovidesPattern MatchinginPython.  It is mostly based onpattern matchingas found inScala.  Its main objective is to deconstruct objects, and thereby check if any
given object fulfills the criteria to be deconstructed in a particular way.This document gives a rough, unpolished overview ofpyPMatch, and its abilities.  Better documentation can be found
in thedoc-folder, in particular theintroduction.  There is also a FAQ further below.pyPMatchrequires at least Python 3.4.ExamplepyPMatchwas initially developed for analysis of Python code via itsAbstract Syntax Tree(AST).  The example below
shows howpyPMatch's pattern matching can be used to implement a very simple code optimiser.  However, there is nothing
special about theast-module frompyPMatch's point of view, and you can equally use it in combination with anything
else.importastfromastimportAdd,BinOp,Numdefsimplify(node):matchnode:caseBinOp(Num(x),Add(),Num(y)):returnNum(x+y)caseBinOp(Num(n=x),Sub(),Num(n=y)):returnNum(x-y)caseast.UnaryOp(ast.USub(),x@Num()):returnNum(-x.n)case_:returnnodeYou will find more examples in theexamples folder; just runrun_example.py.There is also some documentation in thedoc-folder, in particular theintroduction.UsageInstall pyPMatchIn order to install thepyPMatchlibrary, simple do:pip install pyPMatchCompile/Execute Code DirectlyIf you simply want to takepyPMatchon a test drive, usepyma_execas shown below.frompmatchimportpama_execmy_code="""from random import randintmatch randint(0, 19):case 0:print("nothing")case 1 | 4 | 9 | 16:print("a square")case 2 | 3 | 5 | 7 | 11 | 13 | 17 | 19:print("a prime")case _:print("some other number")"""pama_exec(my_code)It is equally possible to retrieve, and inspect the code generated bypyPMatch, using the functionpama_translate:frompmatchimportpama_translatemy_code="""from random import randintmatch randint(0, 19):case 0:print("nothing")case 1 | 4 | 9 | 16:print("a square")case 2 | 3 | 5 | 7 | 11 | 13 | 17 | 19:print("a prime")case _:print("some other number")"""code,match_code=pama_translate(my_code)print(code)# the translation of the original codeprint("="*80)print(match_code)# additional code for the actual matchingImport Code From Python ModulesIt is probably more convenient to install the auto import hook, so that all modules in your package/project are
compiled using thepyPMatch-compiler (if they contain acasestatement, that is).  The auto import is installed
directly through the import ofenable_auto_import.frompmatchimportenable_auto_importfromrandomimportrandintimportmy_modulemy_module.test_me(randint(0,19))The contents ofmy_module.pyis then something like:deftest_me(arg):matcharg:case0:print("nothing")case1|4|9|16:print("a square")case2|3|5|7|11|13|17|19:print("a prime")caseint():print("some other number")case_:print("please provide an integer")Decorate FunctionsIf you do not wantpyPMatchto mess with your code, you can still use the pattern matching in the form of function
decorators.  You put the pattern as a string into the decorator.  The function itself then takes the variables of the
pattern as parameters.frompmatchimportcase@case("17")deftest_me():print("This is correct!")@case("11 | 13 | 17 | 19")deftest_me():print("At least, it's still a prime number")@case("i @ int()")deftest_me(i):print("The result",i,"is wrong")@case("x")deftest_me(x):print("Not even an integer?",x)test_me(sum([2,3,5,7]))NB:Using decorators is, after all, not a particularly good idea for this library.  The reason is that, in
contrast to pre-compiling modules, not all names can be properly resolved.  You might therefore end up with some
surprises, or even crashes.How To Write PatternsPatterns can be expressed using the elements described below.As mentioned above:not everything is fully implemented and tested, yet!  In particular, there is only limited
support forA + Bat the moment.Foo()matches all instances of the classFoo;Foo(A, B, C)deconstructs an instance ofFoo, which must yield three values, which then must match the patternsA,B, andC, respectively;Foo(egg=A, ham=B)matches all instances ofFoo, where the attributesegg, andhammatch the patternsAandB, respectively;12,'abc',Trueand other constants match a value if the value is equal to the constant;{ 'a': A, 'b': B }matches if the value has an element'a', as well as an element'b', which matchAandB, respectively.  The value can be dictionary, but it does not have to be.  You can also check for specific
elements within a list, say, using{ 2: A, 5: B };{'RE'}matches if the value is a string that conforms to the regular expression given;{foo}matches any valueVof type string, for whichV.isfoo()evaluates toTrue.  For instance,{lower}will match any string for whichV.islower()is true;A | B | Cmatches if at least one of the patternsA,B,Cmatches;[A, B, C, ..., D, E]matches any sequence where the first three elements matchA,B, andCand the last two
elements matchD, andE, respectively.  This also includes Python's usual iterator unpacking, such as[a, b, *c, d], which is interpreted as[a, b, c @ ..., d];A + Bmatches a string if it can be decomposed into the partsAandB.  For instance,'(' + x + ')'matches
any string that has some text enclosed in parentheses, and returns the middle part asx;x @ Amatches if the patternAmatches, and binds the value to the variablexif the entire match is
successful;_is a wildcard that matches everything;*_and...are wildcards used in sequences, usually with the exact same meaning;xis an abbreviation forx @ _, matches everything, and binds it tox.There are some special cases, and limitations you should be aware of:Any variablexcan only be bound once inside a single pattern (acasestatement).  It is legal to reusexin differentcasestatements, but you cannot have something likeFoo(x, x).  If you need to test if both
values inFooare equal, useFoo(x, y) if x == yinstead;You cannot bind anything inside an alternative.  Hence,A|(x @ B)|Cis illegal;It is not possible to bind anything to the wildcard_.  While_is a regular name in Python, it has special
meaning inpyPMatchpatterns.  Something like_ @ Ais, however, not illegal, but equivalent toA();Even though the ellipsis...is a 'normal value' in Python, it has a special meaning inpyPMatchas a wildcard;If you want to make sure you have adictionarywith certain keys/values,{ ... }will not suffice.  Use the
syntaxdict({ 'key': value, ... })instead;Instead of writing a regular expression on your own, you can use{int}, or{float}to check if a string value
contains anint, or afloat, respectively;pyPMatchdoes not look at the names involved.  If a name is followed by parentheses as inFoo(), the name is taken
to refer to a class/type, against which the value is tested.  Otherwise, the name is a variable that will match any
value.  This means that the patternstrwill match everything and override the variablestrin the process,
whilestr()will test if the value is a string;There are a few exceptions to the last rule.  Since name bindings are illegal in alternatives, anyway, you can writeA|B|Cas an abbreviation forA()|B()|C().  Furthermore,x @ Ais interpreted asx @ A(), since it makes no
sense to bind two distinct variables to the exact same value;Since a variable cannot be of the forma.b, an attributea.bby itself is equivalent toa.b();3 | ... | 6is an abbreviation for the sequence3|4|5|6.  This syntax can be used with integers, and characters
(single-character strings).  Thus, you can also write'a' | ... | 'z', for instance.  Note, that here you need to
write the ellipsis, and cannot use the otherwise equivalent token*_.RoadmapFull support for regular expressions and string matchingTest suitesDocumentation, tutorialsThe Two Versions of thecaseStatementThere are two version of thecasestatement.  You can either usecaseinside amatchblock, or as a standalone
statement.Inside amatchblock, which is compared against the patterns is specified bymatch.deffoo(x):matchx:case'a'|...|'z':print("Lowercase letter")case'0'|...|'9':print("Digit")case_:print("Something else")The same could also be written without thematch.  In that case, you need to specify the value to be tested against
the pattern.  This done using theassyntax.  There is a difference, though.  The standalonecasestatements will
all be tested, so that we explicitly need to usereturnin order to avoid printing"Something else"for everything.deffoo(x):casexas'a'|...|'z':print("Lowercase letter")returncasexas'0'|...|'9':print("Digit")returncasexas_:print("Something else")At the moment, you cannot put standalonecaseinside amatchblock, and, of course, you cannot use acasewithout
specifying the value outside amatchblock.FAQCan I UsepyPMatchin My Project?Yes,pyPMatchis released under theApache 2.0 license, which should allow you to freely usepyPMatchin your
own projects.  Since the project is currently under heavy development, the pattern matching might fail in unexpected
ways, though.In order to provide this new syntax for pattern matching,pyPMatchneeds to translate your code before Python's own
parser/compiler can touch it.  But, the translation process is design to only modify the bare minimum of your original
Python code.  No commends are removed, no lines inserted or deleted, and no variables or functions renamed.  But sincecaseandmatchhave become keywords, there is a possible incompatibility with your existing code.In addition tocaseandmatch,pyPMatchintroduces two more names:__match__, and__matchvalue__, respectively.
It is very unlikely, though, that your program uses either of these names.Why Yet Another Pattern Matching Library/Proposal?There have been discussions about adding aswitchstatement, or even pattern matching toPythonbefore (see, e.g.,PEP 3103).  Hence,pyPMatchis not an new idea.  In contrast to most
discussion I am aware of so far, this project differs in that my focus is not on the exact syntax, but more on getting
the semantics right.  And, at the end of the day, I just needed (or let's say 'strongly desired') pattern matching
for other projects I am working on.As such,pyPMatchshows how full pattern matching can be integrated with Python, but there is no claim whatsoever that
the syntax used here is the best possible alternative.Why Not Just Use Regular Expressions?Regular expressions are great if you want to match a string, say.  The pattern matching we provide here, however,
works on general Python objects, and not on strings.  It is more akin to something likeisinstance, orhasattrtests in Python.How Do I Check If a Value Has a Certain Type?Due to Python's syntax, something likes: strwill not work in order to specify thatsshould be of typestr.
What you would usually do in Python is something likeisinstance(value, str), which translates directly to:casestr():print("We have a string!")Make sure you put the parentheses after thestr, as these parentheses tellpyPMatchthatstris supposed to be a
class against which to test, and not a new name for the value.How Do I Check If a Value Has a Certain Attribute?If you do not care about the class, or type, of an object, but only about its attributes, use the wildcard_as the
class name.  The algorithm will then omit theisinstancecheck, and just test if the object's attributes fulfill the
given conditions - which in this case is simply that there is an attributeegg, which can be anything.case_(egg=_):print("We have something with an attribute 'egg'.")The example above will be translated to a simple test of the formhasattr(value, 'egg').Can I Nest The Match/Case Structures?Basically, yes, you can.  The only real limitation here is that you cannot put amatchdirectly inside anothermatch, whereas it is no problem to put amatchinside a case.  That is to say that the following will fail:matchx:matchy:casez:The reason for this is thatmatchputs the value of the expressionxinto a local variable (and has some further
book-keeping).  The secondmatchmesses this book-keeping up, and replacesxbyy, so that subsequent tests fail.
On the other hand, there is hardly any reason why amatchinside anothermatchshould make sense, anyway.At the moment, nesting is not yet fully implemented, though.  As long you put the match/case structures in separate
functions, there is never a problem.Is This Pattern Matching Library Efficient?The primary objective of this library is correctness, not efficiency.  Once everything runs, there is still time to
worry about improving the performance of the library.  However, there are some strong limitations to how efficient
pattern matching can be done in Python.Since the matching algorithm must analyse various objects, and classes, each time a matching is performed, there are
certainly limitations to the performance a pattern matching algorithm can deliver in Python.  If you have something
like in the code snippet below, the algorithm must test, ifmy_valueis an instance ofFoo, if it has (at least)
the attributeseggsandham, and if the value of the attributeeggsis123.matchmy_value:caseFoo(eggs=123,ham=x):print("A Foo with 123 eggs has ham",x)In statically compiled languages it is possible to test only once (during compilation) if classFoohas attributeseggsandham.  In Python, however, even the classFoorefers to might change, so that we need to test everything
upon each matching attempt.Another limitations is due to the factpyPMatchtries to minimize the amount your code needs to be changed.  This means
that eachcasestatement is treated in isolation from all others, and it is therefore not possible to factor out
common parts.  Again, there is certainly room for further improvement, but it is not a priority ofpyPMatch.Will It Break My Code If I Usecaseandmatchas Variable Names?There is, of course, always a danger thatpyPMatch's compiler will mis-identify one of your variables as amatch,
orcasestatement.  However, in order to be recognised as a statement, either keyword (case,match) must be the
first word on a line, and it cannot be followed by a colon, or an operator (such as an assignment).  So, if you have
a function calledcase, the function callcase(...)might be interpreted as acasestatement, but an assignment
likecase = ..., say, will not.Why Did You Use@for Name Bindings Instead of:=?Python 3.8 will introduce assignment expressions (seePEP 572).  It would
therefore be natural to usex := Ainstead ofx @ Afor name bindings.In fact, I am happy to add full support for:=.  At the time of writing, however,:=is not yet a valid token in
Python.  Using only:=would mean thatpyPMatchrequires at least Python 3.8, while@has already become a valid
operator in Python 3.5PEP 465.Why1 | ... | 9Instead of the Simpler1 ... 9?The entire syntax of patterns inpyPMatchis based on standard Python syntax.  Even though the patterns are semantically
nonsense, they are syntactically valid.  The sequence1 ... 9, however, is not a valid sequence in Python, and would
issue a syntax error.There are various reasons for wanting patterns to be valid Python syntax.  One of them is thatpyPMatchgets away with
much less parsing work on its own.Apart from this issue of pragmatics, writing1 | ... | 9seems clearer to me, since1 ... 9could also mean that
the value has to be the sequence1, 2, 3, ..., 9itself.  This is, however, a matter of personal taste, and thus
debatable.Why Are There Two Versions ofcaseStatements?Pattern matching does usually not only come in the form ofmatchblocks.  At times, we only want to deconstruct a
single value.  Python already supports this in part through assignments likea, b, *c = x.  Using the standalone
version ofcase, you could write this in the formcase x as (a, b, *c):.  However, thecasestatement can do much
more than Python's assignment operator.On the other hand, while developing the library, I wondered if it possible to give meaning tocaseeven outside amatchblock, so as to make the entire syntax as orthogonal, and as flexible as possible.AspyPMatchis kind of a prototype, in the end, the standalone variant ofcasemight not survive, and not make it into
subsequent versions.  For the moment, it remains there to fully test its usefulness.Why ismatchNot an Expression as in Scala?WhileScala's syntax and semantics are based on expressions,Python's is not.  Compound statements likewhile,if,foretc. are, as a matter of fact, never expressions in Python, but clearly statements without proper value.
Since bothmatchandcasestatements, as implemented here, are obviously compound statements, it would feel very
wrong for Python to try, and make them expressions.Why Do I Have to Usecase _Instead Ofelse?The implementation ofpyPMatchis focused on minimising the rewriting of any Python code, or module.  It will only
translatecase, andmatch, statements where it is pretty certain that such a statement is meant in the first
place, leaving all your code around it untouched.If we were to useelse, this means that we would have to put a lot more effort in making sure that noelseis
replaced where it should remain, leading to longer and more complex code.  Moreover, the individualcasestatements
in amatchblock are actually not linked, but stand as individual statements for themselves.  Usingelseraises
therefore a few additional questions concerning the semantics, which need proper answering.So, in short: usingelsewould lead to a more brittle syntax with quite a few corner cases not covered.How About Some Proper Documentation?First priority is currently given to getting the library fully operational, and adding various test cases.  Once that
is complete, documentation will follow (and, after all, there is already a rather long README with lots of information,
as well as several examples).  If you have a specific question or concern, open an issue, or write to me directly.ContributorsTobias Kohn