How to use values stored in variables as case patterns?

Question:

I’m trying to understand the new structural pattern matching syntax in Python 3.10. I understand that it is possible to match on literal values like this:

def handle(retcode):
    match retcode:
        case 200:
            print('success')
        case 404:
            print('not found')
        case _:
            print('unknown')

handle(404)
# not found

However, if I refactor and move these values to module-level variables, it results in an error because the statements now represent structures or patterns rather than values:

SUCCESS = 200
NOT_FOUND = 404

def handle(retcode):
    match retcode:
        case SUCCESS:
            print('success')
        case NOT_FOUND:
            print('not found')
        case _:
            print('unknown')

handle(404)
#  File "<ipython-input-2-fa4ae710e263>", line 6
#    case SUCCESS:
#         ^
# SyntaxError: name capture 'SUCCESS' makes remaining patterns unreachable

Is there any way to use the match statement to match values that are stored within variables?

Asked By: jakevdp

||

Answers:

If the constant you’re testing against is a dotted name, then it should be treated as a constant instead of as the name of the variable to put the capture in (see PEP 636 # Matching against constants and enums):

class Codes:
    SUCCESS = 200
    NOT_FOUND = 404

def handle(retcode):
    match retcode:
        case Codes.SUCCESS:
            print('success')
        case Codes.NOT_FOUND:
            print('not found')
        case _:
            print('unknown')

Although, given how python is trying to implement pattern-matching, I think that for situations like this it’s probably safer and clearer code to just use an if/elif/else tower when checking against constant values.

Answered By: Green Cloak Guy

Aside from using literal values, the Value Patterns section of PEP 635 mentions the use of dotted names or the use of guards. See below for comparison:

Literal values

def handle(code):
    match code:
        case 200:
            print('success')
        case 404:
            print('not found')
        case _:
            print('unknown')

References:

Dotted names

Any dotted name (i.e. attribute access) is interpreted as a value pattern.

class StatusCodes:
    OK = 200
    NOT_FOUND = 404

def handle(code):
    match code:
        case StatusCodes.OK:
            print('success')
        case StatusCodes.NOT_FOUND:
            print('not found')
        case _:
            print('unknown')

References:

Guards

[A] guard is an arbitrary expression attached to a pattern and that must evaluate to a "truthy" value for the pattern to succeed.

SUCCESS = 200
NOT_FOUND = 404

def handle(code):
    match code:
        case status if status == SUCCESS:
            print('success')
        case status if status == NOT_FOUND:
            print('not found')
        case _:
            print('unknown')

References:

Answered By: Gary

Hopefully I can help shed some light on why bare names work this way here.

First, as others have already noted, if you need to match values as part of your patterns, you can do so by:

  • Matching supported literals, like numbers, strings, booleans, and None
  • Matching qualified (dotted) names
  • Using additional tests in guards (which are separated from patterns by if)

I fear that we (the PEP authors) probably made a small error by including this toy snippet in an early tutorial… it’s since gone a bit viral. Our goal was to lead with the simplest possible example of pattern matching, but we instead seem to have also created a confusing first impression for many (especially when repeated without context).

The most overlooked word in the title of these PEPs is "structural". If you’re not matching the structure of the subject, structural pattern matching probably isn’t the right tool for the job.

The design of this feature was driven by destructuring (like iterable unpacking on the LHS of assignments, but generalized for all objects), which is why we made it very easy to perform the core functionality of extracting out parts of an object and binding them to names. We also decided that it would also be useful to allow programmers to match on values, so we added those (with the condition that when the values are named, they must be qualified with a dot, in order to distinguish them from the more common extractions).

Python’s pattern matching was never really designed with the intent of powering C-style switch statements like this; that’s been proposed for Python (and rejected) twice before, so we chose to go in a different direction. Besides, there is already one obvious way to switch on a single value, which is simpler, shorter, and works on every version of Python: a good-ol’ if/elif/else ladder!

SUCCESS = 200
NOT_FOUND = 404

def handle(retcode):
    if retcode == SUCCESS:
        print('success')
    elif retcode == NOT_FOUND:
        print('not found')
    else:
        print('unknown')

handle(404)

(If you’re really concerned about performance or need an expression, dispatching from a dictionary is also a fine alternative.)

Answered By: Brandt Bucher

Python’s match is much more than a simple switch statement. If you use bare what you consider "variable names", they are actually going to be Capture Patterns. as per definition in PEP no. 634

Besides the fact that you should probably not use match for your use case, you have to use qualified (dotted) names in one of the following ways:

#1 Flat Object

statuses = object()
statuses.success = 200
status.not_found = 404

def handle(retcode):
    match retcode:
        case statuses.success: print("Success")
        case statuses.not_found: print("Not found")

#2 Object Oriented Programming

class StatusValues:
    success = 200
    not_found = 404

def handle(retcode):
    match retcode:
        case StatusValues.success: print("Success")
        case StatusValues.not_found: print("Not found")

#3 Simple qualified locals()/globals() access

I’ve developed the match-ref library which allows you to access any local or global variable in- or outside of any function, simply using the ref. prefix.

from matchref import ref
import random

SUCCESS = 200
NOT_FOUND = 404

def handle(retcode):
    random_code = random.randint(600,699)

    match retcode:
        case ref.SUCCESS: print("Success")
        case ref.NOT_FOUND: print("Not found")
        case ref.random_code: print("OK, you win!")

As you can see, ref automatically resolved variables from your local and global namespaces (in this order). There’s no additional setup necessary.

If you don’t want to use 3rd-party libraries, you can see a slightly similar no-libraries version below.

#4 Qualified locals()/globals() access without 3rd-party libs

locals() and globals() are built-in functions in Python which return a dict containing all your variable names mapped to their respective values. You need to be able to access the dict’s values using dotted syntax, since match also does not support dictionary access syntax. You can therefore write this simple helper class:

class GetAttributeDict(dict):
    def __getattr__(self, name):
        return self[name]

and use it like so:

import random

SUCCESS = 200
NOT_FOUND = 404

def handle(retcode):
    random_code = random.randint(600, 699)
    globs = GetAttributeDict(globals())
    locs = GetAttributeDict(locals())

    match retcode:
        case globs.SUCCESS: print("Success")
        case globs.NOT_FOUND: print("Not found")
        case locs.random_code: print("OK , you win!")

#5 Module Access

Given that you seem to intend to re-use your status codes (because otherwise you could inline them in your case s), you might consider using separate modules for this.

constants.py:

SUCCESS = 200
NOT_FOUND = 404

main.py

import constants

match retcode:
    case constants.SUCCESS: ...
    ...

Again, you might want to reconsider if you want to use match at all.

Answered By: fameman

python > 3.10 lets you handle case patterns more efficiently.

| and if statements can be used as well.

using |

match name: 
    case "example_111" | "example_222": 
        return f"Hello {name}" 
    case _: 
        return "Bye"

using if statement

def get_product_info(make, in_dollar): 

    match make:

        case "product_111" if in_dollar: 
            return "10000 $"

        case "product_222" if not in_dollar:
            return "10000*73 INR"

        case _: 
            return "error"
Answered By: SuperNova

Is there any way to use the match statement to match values that are stored within variables?

You can do this in other languages with immutable data types.

The match statement in python rebinds the values to the variables when possible, so they will match.

To avoid this, use literals or dotted structures. Sometimes it is just easier to use guards or an if statement.

Answered By: mit