Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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)?
like image 869
upe Avatar asked Oct 29 '25 01:10

upe


1 Answers

As specified, mapping captures are used to match on some substructure of keys/values. An empty dict is a "subdict" of any non-empty dict, so the pattern {} may capture any non-empty dict.

You can add a guard to specify that there are no "extra" items present:

>>> def match_empty(m):
...     match m:
...         case []:
...             print("empty list")
...         case {**extra} if not extra:
...             print("empty dict")
...         case _:
...             print("not empty")
... 
>>> match_empty({})
empty dict
>>> match_empty({1:1})
not empty

For some justification of why mapping patterns allow extra items by default, refer to PEP 635 – Structural Pattern Matching: Motivation and Rationale:

The mapping pattern reflects the common usage of dictionary lookup: it allows the user to extract some values from a mapping by means of constant/known keys and have the values match given subpatterns. Extra keys in the subject are ignored even if **rest is not present. This is different from sequence patterns, where extra items will cause a match to fail. But mappings are actually different from sequences: they have natural structural sub-typing behavior, i.e., passing a dictionary with extra keys somewhere will likely just work.

Finally, there was a slight misconception in the question that may be worth pointing out here:

Matching the constructors even breaks the empty list matching

This is not "matching the constructors". No actual list or dict instance will be created by the list() or dict() written there. It's just the normal way to match on types. It can't be just list or dict because that would be a name binding.

like image 97
wim Avatar answered Oct 30 '25 17:10

wim