On the case of single-line match statements

PEP 622 - the PEP for match statements has an idea in the “Deferred” section: to create a one-line equivalent of a simple match case:

if match node as CallExpr(callee=Name(value=call), args=[Name(value=arg)]):
    ...  # Continue special-casing 'call' and 'arg'
...  # Follow with common code

I’d like to raise the idea back again, with a few examples:

if match item as {'command': command, 'value': value}:
    ... # process command with given value
else:
    raise ValueError(f"Invalid input: {item}")
if match status as internal_error if status >= 500:
    print("Internal server error")
else:
   ... # process response

Would love to hear your thoughts on the usefulness of this.

3 Likes

The PEP Deferred Ideas section talks about a couple of possible syntaxes and isn’t formatted as a proposal. It would be best to clarify exactly what syntax you propose.


I’m not aware of very much discussion about this particular deferred idea. It was there since the first published draft of the PEP and I don’t think anyone argued for it to not be deferred.
So! The door is wide open for someone to come in with some really good examples and motivation for implementing a “one off match” syntax.

However, I find these examples to be poor as justification. I don’t understand from them why this syntax might be useful.

The second one seems to have the syntax slightly wrong, I think?[1] I believe your intent was

match status as internal_error if status >= 500:
    ...
...

or

if match status as internal_error and status >= 500:
    ...
else:
    ...

but in either case, why are we using a match at all? Why not omit the internal_error variable and write this?

if status >= 500:
    ...
else:
    ...

Could you share some real examples from code where you’ve wanted this, and where a trivial alternative spelling is not possible?


  1. Because the proposed syntax is unclear, I can’t say that definitively it’s incorrect. ↩︎

Destructuring assignment is where this would be most convenient, as in the first example shown:

if match item as {'command': command, 'value': value}:
    process(command, value)
else:
    raise ValueError(f"Invalid input: {item}")

That said, just using a match statement isn’t terrible:

match item:
    case {'command': command, 'value': value}:
        process(command, value)
    case _:
        raise ValueError(f"Invalid input: {item}")

Saving a single line and indentation level isn’t a super compelling justification for new syntax.

The idea gets a bit more interesting in while loops, though.

4 Likes

I have frequently been using dictionnaries as “switches”, that I explicitly call ‘switch_’ :

switch_inputs = {'command': command, 'value': value}
...
process(switch_input[item])

It elegantly fits global optional parameters management and is easy to maintain, if you know what it is meant to do.

The drawback is that in case of KeyError the traceback shows only the name of the dict (hopefully) in the exception line, not in the exception message, so it might be unclear that the dict was meant to be used as a switch… and I am not sure this switch concept is intuitive and clear for everyone, btw.

I think an if match construct would be most useful when a failed match is not an issue.

Reusing your examples, I find if match here works better:

if match item as {'command': command, 'value': value}:
    process(command, value)

instead of:

match item:
    case {'command': command, 'value': value}:
        process(command, value)
    case _:
        pass

Or

if isinstance(item, dict) and 'command' in item and 'value' in item:
    command = item['command']
    value = item['value']
    process(command, value)

Or

try:
    command = item['command']
    value = item['value']
    process(command, value)
except (TypeError, KeyError):
    pass
1 Like

This should do the trick as well (you don’t have to provide a default case):

match item:
    case {'command': command, 'value': value}:
        process(command, value)

You save a single line with the if match proposal. Not sure whether that’s worth the trouble.

3 Likes

You also save a level of indentation, and the extra level of indentation is the one thing that has made me reluctant to use match case in python quite a few times.

No-one’s ever going to implement a whole new syntax construct just to save a level of indentation, though…

  1. I don’t like the proposed role of the as keyword, deviating from the role of as in the existing match/case syntax.
  2. I believe that a more general approach – which would make it possible to use pattern matching in expressions – could have a better chance to appear useful enough to be worth implementing.

It could be something along the lines of the following expression syntax:

SUBJECT_EXPR matches PATTERN (without any guard part!)

The value of the entire expression would always be just True of False.

The semantics of the new construct would be such that this:

result = SUBJECT_EXPR matches PATTERN

…would be equivalent to this:

match SUBJECT_EXPR:
    case PATTERN:
        result = True
    case _:
        result = False

It could be used where any Boolean expression can.

Examples:

if item matches {'command': command, 'value': value}:
    print(command, value)
if status matches MyStatus(code) and code >= 500:  # no guard, just two expressions forming an `and` expression
    print("Internal server error")
else:
   ... # process response
c = 0
key_to_obj = {}
row = first_row()
while row matches (key, MyObj(n=int(n)) as obj):
    key_to_obj[key] = obj
    c += n
    row = next_row()
extracted_even_id_values = [
    value
    for item in generate_items()
    if item matches {'id': ident, 'value': value} and not ident % 2
]
process_item(myitem, is_valid=(myitem matches {'id', _, 'value': _}))
def should_item_be_consumed(item, seen_ids):
    return (item matches {'id', int(ident), 'value': str()}
            and ident not in seen_ids)
3 Likes

I’m liking this matches operator a lot. It is indeed a more elegant solution to Expand structural pattern matching-like syntax to assignments/generalized unpacking that I proposed a while back.

And thanks for putting together some good usage examples. Like @ncoghlan pointed out, the usefulness of such an operator is most apparent as a while loop condition, making the intent of the loop that much more immediately understood than an equivalent while True: match ...: case ...: if ...: break.

Great example here as well. Embedding the matches expression in a filter of a list comprehension reads better than an equivalent for loop with a match-case statement conditionally appending to a list IMHO.

To be sure, this operator is syntax sugar just like comprehensions are. But good syntax sugar can help convey the intent of code logics better than equivalent primitive constructs when used in right places.

I hope this can gain more support from the devs.

2 Likes