|
|
Log in / Subscribe / Register

A more generalized switch statement for Python?

By Jake Edge
July 7, 2016

Many languages have a "switch" (or "case") statement to handle branching to different blocks based on the value of a particular expression. Python, however, does not have a construct of that sort; it relies on chains of if/elif/else to effect similar functionality. But there have been calls to add the construct over the years. A recent discussion on the python-ideas mailing list demonstrates some of the thinking about what a Python switch might look like—it also serves to give a look at the open language-design process that typifies the language.

There are two PEPs that have proposed the feature over the years: Marc-André Lemburg's PEP 275 from 2001 and Python benevolent dictator for life Guido van Rossum's PEP 3103 from 2006. The latter came about due to some differences of opinion about the behavior of the feature and how it would be implemented. Ultimately, though, both were rejected based on an informal poll: "A quick poll during my keynote presentation at PyCon 2007 shows this proposal has no popular support. I therefore reject it.", Van Rossum said in his PEP.

In a discussion on type hinting for the pathlib module (and related standard library routines that can use and return either str or bytes types), the lack of a Python switch reared its head again. That initial thread centered around using the AnyStr annotation for the __fspath__() protocol, which can return either bytes or str. In a post in that thread, Van Rossum mused about adding a switch statement, though he called it "match":

The one thing that Python doesn't have (and mypy doesn't add) would be a match statement. The design of a Pythonic match statement would be an interesting exercise; perhaps we should see how far we can get with that for Python 3.7.

That post generated a few responses in favor of looking at the feature, so Van Rossum soon started a new "match statement brainstorm" thread. He said that Python might well benefit from moving beyond the "pre-computed lookup table" approach that both of the earlier PEPs had taken and learn from what other languages have done (notably, Haskell). A Python match statement (though he used a switch keyword in his examples) could possibly do quite a bit more than had been envisioned earlier:

A few things that might be interesting to explore:
  • match by value or set of values (like those PEPs)
  • match by type (isinstance() checks)
  • match on tuple structure, including nesting and * unpacking (essentially, try a series of destructuring assignments until one works)
  • match on dict structure? (extension of destructuring to dicts)
  • match on instance variables or attributes by name?
  • match on generalized condition (predicate)?
The idea is that many of these by themselves are better off using a classic if/elif/else structure, but a powerful matching should be allowed to alternate between e.g. destructuring matches and value or predicate matches.

The idea of "destructuring" is to pull out the values in a composite type (such as a tuple), either using positional operators or using attribute names for types like the collections.namedtuple type. That could potentially be extended to destructure dictionaries by key name or, perhaps, positionally.

His post had some "strawman syntax" for how tuple destructuring in a switch statement might work, along with a "demonstration" of how it would operate given different kinds of input:

def demo(arg):
    switch arg:
        case (x=p, y=q): print('x=', p, 'y=', q)
        case (a, b, *_): print('a=', a, 'b=', b)
        else: print('Too bad')
That would add two new keywords to Python (switch and case), which is somewhat tricky to do since those names might be used for variables in existing programs. The first case is perhaps somewhat confusing as it will attempt to extract the attributes x and y and assign them to the p and q variables—which seems rather backward from the way the assignment operator normally works. The second would simply destructure the argument into a and b, leaving any other members of the sequence in the throwaway _ variable.

Taking his example further, Van Rossum showed how it all might work:

Now suppose we had a Point defined like this:
Point = namedtuple('Point', 'x y z')
and some variables like this:
a = Point(x=1, y=2, z=3)
b = (1, 2, 3, 4)
c = 'hola'
d = 42
then we could call demo with these variables:
>>> demo(a)
x= 1 y= 2
>>> demo(b)
a= 1 b= 2
>>> demo(c)
a= h b= o
>>> demo(d)
Too bad

He did note the "slightly unfortunate outcome" for the string (since strings are treated as sequences of one-character strings in Python).

As might be guessed, that strawman syntax led to some other suggestions. Several commented on the attribute-extraction case with its odd-looking "assignment" construct. Nick Coghlan suggested an alternate formulation:

For the destructuring assignment by attribute, I'd suggest the "value as name" idiom, since it's not quite a normal assignment, as well as a leading "." to more readily differentiate it from iterable unpacking:
        [...]
        case (.x as p, .y as q): print('x=', p, 'y=', q)

In the brainstorming post, Van Rossum had also challenged others "to fit simple value equality, set membership, isinstance, and guards into that same syntax." Coghlan had some ideas on that, but he also suggested a new operator, of sorts:

If we went down that path, then the "assign if you can, execute this case if you succeed" options would presumably need an explicit prefix to indicate they're not normal expressions, perhaps something like "?=":
    switch expr as arg:
        case ?= (.x as p, .y as q): print('x=', p, 'y=', q)
        case ?= (a, b, *_): print('a=', a, 'b=', b)
        case arg == value: ...
        case lower_bound <= arg <= upper_bound: ...
        case arg in container: ...
        else: print('Too bad')

The first two cases are equivalent to Van Rossum's example, but the next three demonstrate testing for equality, a range of values, and membership in a container type. In fact, Coghlan said, that unpacking syntax might be extended to the left-hand side of assignments as well as add other unpacking options:
    (.x as p, .y as q) = expr
In a similar vein, item unpacking might look like:
    (["x"] as p, ["y"] as q) = expr

Franklin Lee also had an extensive set of suggestions, but several in the thread thought some of them were overkill. Paul Moore suggested allowing an arbitrary expression for the switch that would be given a name for use in the case statements (syntax that Coghlan also adopted):

    switch expr as name:
Though Moore pointed out that the same effect could be had by simply assigning to the name just above the switch. He also wondered about what would happen if multiple case statements matched. Van Rossum made it clear that each case would be tried in order until one succeeds. Unlike switch statements in other languages (e.g. C), a Python switch would not allow falling through into other case blocks—either zero or one case block will be executed. In fact, no matching case would raise some kind of MatchError exception.

But Joao S. O. Bueno had a fundamental concern about the need to add a switch at all: "I still fail to see what justifies violating The One Obvious Way to Do It which uses an if/elif sequence". Van Rossum agreed to a certain extent, but noted that there are a number of match operations that are difficult to write using if statements. For example:

[...] combining an attempted tuple unpack with a guard, or "instance unpack" (check whether specific attributes exist) possibly combined with a guard. (The tricky thing is that the guard expression often needs to reference to the result of the unpacking.)

There might be some other interesting possibilities when combining matching with type annotations, he said. Overall, though, "it's about the most speculative piece of language design I've contemplated in a long time".

Michael Selik noted that many of the matching features are already available for if statements. The missing piece is something like the ?= operator to allow trying the destructure operations without causing an exception if they fail—they would simply return false. He provided an example:

def demo(arg):
    if p, q ?= arg.x, arg.y: # dict structure
    elif x ?= arg.x and isinstance(x, int) # assignment + guard
    elif a, b, *_ ?= arg: # tuple structure
    elif isinstance(arg, Mapping): # nothing new here
While Van Rossum thought there were some advantages to that syntax, he was unsure about using "?" in the conditional assignment operation; it had been "rejected by this group in the past for other conditionalisms".

But Greg Ewing (and others) were not particularly pleased with the suggestion: "the above looks like an unreadable mess to me". The problem, as Moore described, is that switch is a focused operation on a single subject, while if statements are not:

The key distinguishing feature for me of a switch/match statement is that it organises the "flow" of the statement differently from if/elif. The if/elif chain says "try this, then that, now something else". There's no implication that the tests are all looking at the same subject - to spot that, you need to be able to see (from the layout of the tests) the actual subject item on each line, and the ?= operator syntax obscures that because "arg" is used differently in each test.

With a switch statement, however, the subject is stated once, at the top of the statement. The checks are then listed one after the other, and they are all by definition checks against the subject expression.

There was more discussion of the ideas, though no real conclusions were drawn. No one reported an in-progress PEP to the list, so there may be no one who feels strongly enough about the feature to take that step. But it is an idea that has recurred in Python circles over the years, so it will not be a surprise to see it pop up again sooner or later. In the meantime, as with many discussions on python-ideas, we get a look inside the thinking of the Python core developers.



to post comments

A more generalized switch statement for Python?

Posted Jul 8, 2016 15:44 UTC (Fri) by gasche (subscriber, #74946) [Link] (3 responses)

Algebraic type and pattern-matching were introduced in the 1970s by the functional language Hope, and were quickly adopted by the ML languages (SML, OCaml, Haskell, F#, etc.; with a graceful adaptation to the object-oriented programming as sealed classes in Scala). I find that the discussions related in this article give the impression of the Python community as rather closed-minded as far as language design is concerned. For an interesting take on the form of programming language illiteracy fostered by self-contained one-language communities, see

Why I hate advocacy
Mark-Jason Dominus, 2000
http://www.perl.com/pub/2000/12/advocacy.html

A more generalized switch statement for Python?

Posted Jul 9, 2016 4:28 UTC (Sat) by marcH (subscriber, #57642) [Link] (2 responses)

Once you've seen in action ML-like pattern matching in some random language (from the ML family or not) and caught actual bugs thanks to it, then people claiming that if/elif/else is "The One Obvious Way to Do It" really sound like they're from the distant past. Maybe still trying to do large-scale development in assembly or something.

Quite surprising from Python: an otherwise fairly high-level language.

A more generalized switch statement for Python?

Posted Jul 10, 2016 18:19 UTC (Sun) by HIGHGuY (subscriber, #62277) [Link] (1 responses)

Pattern matching in those languages is great... because of the very strong typing (and automatic type deduction).
Putting this kind of matching with python's duck-typing is like trying to have your cake and eat it too. It's just going to cause even more subtle bugs that are hard to diagnose or find.

A more generalized switch statement for Python?

Posted Jul 10, 2016 20:14 UTC (Sun) by flussence (guest, #85566) [Link]

The very least they could do here is learn to not repeat the mistakes Perl 5 did in trying to add a switch construct to the language.

The examples given all look very *pretty*, which is what one would expect Python culture to focus on, but they're harder to *understand*.

A more generalized switch statement for Python?

Posted Jul 8, 2016 17:47 UTC (Fri) by nybble41 (subscriber, #55106) [Link]

> The if/elif chain says "try this, then that, now something else". There's no implication that the tests are all looking at the same subject ... With a switch statement, however, the subject is stated once, at the top of the statement. The checks are then listed one after the other, and they are all by definition checks against the subject expression.

This is true of switch statements in C, but not every other language. For example, in Haskell the top-level patterns are all matched against a single value, but you can also add "guards" which add arbitrary conditions and even nested pattern matches:

case x of
(4, y) -> "case 1"
(x, 7) -> "case 2"
(x, y) | x < y -> "case 3"
_ | z == True -> "case 4"
_ | [z, w] <- list -> "case 5"

Haskell also has "view patterns" as a language extension which allow things like matching on dictionaries:

case dict of
(M.lookup "key" -> Just value) -> print value
_ -> putStrLn "No match!"

The left-hand side of the view pattern is an arbitary function which is given the value at that position as a parameter. The right-hand side is a pattern matched against the function's return value. Of course, in this case you could just as easily write `case M.lookup "key" dict of ...`; it becomes more useful when you want to match against multiple keys or use the view pattern in a pattern synonym.

F# has a similar guard syntax as well as "active patterns", which are similar to Haskell's view patterns combined with pattern synonyms.


Copyright © 2016, Eklektix, Inc.
This article may be redistributed under the terms of the Creative Commons CC BY-SA 4.0 license
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds