pythonpattern-matchingpython-3.10

Using pattern matching with a class that inherits from str in Python 3.10


In a parser library I maintain, I have some classes that inherit from str to manage parsed strings and parsed symbols. This has been working well for a long time, but with Python 3.10, someone requested being able to use match and case on these classes. I have constructed an example script that shows how this is failing:

class Binge(str):
    def __eq__(self, other):
        return self.__class__ == other.__class__ and str.__eq__(self, other)

    def __ne__(self, other):
        return not self == other


# These two asserts are important for how the class is used
assert Binge('a') == Binge('a')
assert Binge('a') != 'a'

# What does it take to then make this work?
matched = False
match Binge("asdf"):
    case Binge("asdf"):
        matched = True
assert matched

If I add:

print(self.__class__, other.__class__)

in the __eq__ function, I see this:

<class '__main__.Binge'> <class 'str'>

Solution

  • Class patterns are generally matched by attributes, key-value pairs, so e.g.:

    match subject:
        case Thing(foo="bar"):
            ...
    

    becomes something like:

    isinstance(subject, Thing) and subject.foo == "bar"
    

    You can get a pattern like Thing("bar") to work, but this requires a __match_args__ class attribute to map from positional arguments to the appropriate attribute, then the pattern becomes something like:

    isinstance(subject, Thing) and getattr(subject, Thing.__match_args__[0]) == "bar"
    

    So for the above patterns to both work correctly, Thing would have to look something like:

    from typing import ClassVar
    
    
    class Thing:
    
        __match_args__: ClassVar[tuple[str, ...]] = ("foo",)
    
        foo: str
    
        def __init__(self, foo: str):
            self.foo = foo
    

    However, as the documentation notes:

    For a number of built-in types (specified below), a single positional subpattern is accepted which will match the entire subject...

    One of those built-in types is str so, because Binge inherits from str, the pattern:

    match subject:
        case Binge("asdf"):
            ...
    

    becomes something like:

    isinstance(subject, Binge) and subject == "asdf"
    

    This explains what you're seeing: case Binge("asdf"):Binge("asdf").__eq__("asdf")False, and the pattern does not match.

    Therefore you cannot have all three of the following: