Search code examples
pythonpython-3.10structural-pattern-matching

How to match an empty dictionary?


Python supports Structural Pattern Matching since version 3.10. I came to notice that matching an empty dict doesn't work by simply matching {} as it does for lists. According to my naive approach, non-empty dicts are also matched (Python 3.10.4):

def match_empty(m):
    match m:
        case []:
            print("empty list")
        case {}:
            print("empty dict")
        case _:
            print("not empty")
match_empty([])           # empty list
match_empty([1, 2])       # not empty
match_empty({})           # empty dict
match_empty({'a': 1})     # empty dict

Matching the constructors even breaks the empty list matching:

def match_empty(m):
    match m:
        case list():
            print("empty list")
        case dict():
            print("empty dict")
        case _:
            print("not empty")
match_empty([])           # empty list
match_empty([1, 2])       # empty list
match_empty({})           # empty dict
match_empty({'a': 1})     # empty dict

Here is a solution, that works as I expect:

def match_empty(m):
    match m:
        case []:
            print("empty list")
        case d:
            if isinstance(d, dict) and len(d) == 0:
                print("empty dict")
                return
            print("not empty")
match_empty([])           # empty list
match_empty([1, 2])       # not empty
match_empty({})           # empty dict
match_empty({'a': 1})     # not empty

Now my questions are:

  • Why do my first 2 approaches not work (as expected)?
  • Is there a way to use structural pattern matching to match only an empty dict (without checking the dict length explicitly)?

Solution

  • Using a mapping (dict) as the match pattern works a bit differently than using a sequence (list). You can match the dict's structure by key-value pairs where the key is a literal and the value can be a capture pattern so it is used in the case.

    You can use **rest within a mapping pattern to capture additional keys in the subject. The main difference with lists is - "extra keys in the subject will be ignored while matching".

    So when you use {} as a case, what you're really telling Python is "match a dict, no constraints whatsoever", and not "match an empty dict". So one way that might be slightly more elegant than your last attempt is:

    def match_empty(m):
        match m:
            case []:
                print("empty list")
            case {**keys}:
                if keys:
                    print("non-empty dict")
                else:
                    print("empty dict")
            case _:
                print("not empty")
    

    I think the main reason this feels awkward and doesn't work good is because the feature wasn't intended to work with mixed types like this. i.e. you're using the feature as a type-checker first and then as pattern matching. If you knew that m is a dict (and wanted to match its "insides"), this would work much nicer.