pythondefensive-programmingpython-3.10structural-pattern-matching

Avoiding accidental capture in structural pattern matching


This example is being discussed as likely "gotcha" when using pattern matching:

NOT_FOUND = 400

retcode = 200
match retcode:
    case NOT_FOUND:
        print('not found')  

print(f'Current value of {NOT_FOUND=}')

This is an example of accidental capture with structural pattern matching. It gives this unexpected output:

not found
Current value of NOT_FOUND=200

This same problem comes up in other guises:

match x:
    case int():
        pass
    case float() | Decimal():
        x = round(x)
    case str:
        x = int(x)

In this example, str needs to have parentheses, str(). Without them, it "captures" and the str builtin type is replaced with the value of x.

Is there a defensive programming practice that can help avoid these problems and provide early detection?


Solution

  • Best practice

    Is there a defensive programming practice that can help avoid these problems and provide early detection?

    Yes. Accidental captures are easily detected by always including what PEP 634 describes as an irrefutable case block.

    In plain language, that means a catchall case that always matches.

    How it works

    Accidental captures always match. No more than one irrefutable case block is permitted. Therefore, accidental captures are detected immediately when an intentional catchall is added.

    Fixing the first example

    Just add a catchall wildcard pattern to the end:

    match retcode:
        case NOT_FOUND:
            print('not found')
        case _:
            pass
    

    That detects the problem immediately an gives the following error:

    SyntaxError: name capture 'NOT_FOUND' makes remaining patterns unreachable
    

    Fixing the second example

    Add a catchall wildcard pattern to the end:

    match x:
        case int():
            pass
        case float() | Decimal():
            x = round(x)
        case str:
            x = int(x)
        case _:
            pass
    

    Again the problem is detected immediately:

    SyntaxError: name capture 'str' makes remaining patterns unreachable