I am trying to understand an inconsistency in the way that pattern matching (match-case statement) appears to work for fractions.Fraction objects. Here is an example (in a Python 3.11.2 environment) which hopefully illustrates it.

>>> x = Fraction(1)
>>> match x:
... case 1 | Fraction(1):
... print('x == 1')
... case _:
... print('x != 1')
x == 1

This is as expected. But if I now replace the initial literal 1 in the case clause with int(1), with x still equal to Fraction(1), I get a TypeError:

>>> x = Fraction(1)
>>> match x:
... case int(1) | Fraction(1):
... print('x == 1')
... case _:
... print('x != 1')
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
TypeError: Fraction() accepts 0 positional sub-patterns (1 given)

This is curious, because 1 == int(1) == Fraction(1), and it is unexpected that replacing the initial literal 1 in the case with int(1) would cause the matching on Fraction(1) to break.

My initial encounter with this error was in the context of overriding __rtruediv__ in a custom subclass of Fraction, where I have to implement a custom behaviour for inversion, i.e. if the other argument is equal to the integer 1, which means matching on the 1 (or int(1)) and Fraction(1).

This is not really a problem as Iâ€™m happy to use 1 | Fraction(1) in the case - itâ€™s suficiently simple. But I was wondering as to the inconsistency with int(1).

Also, the value 1 is not special here - the same error occurs for all other integer values Iâ€™ve tried.

Fraction(1) does not do what you think it does. It checks if val if an instance of Fraction and then looks up Fraction.__match_args__, which in this is empty, which means you canâ€™t use positional arguments.

Even if it werenâ€™t empty, i.e. if it were ('numerator', 'denominator)', it still wouldnâ€™t do what you want: It would check if the numerator was equal to 1 and ignore the denominator.

case patterns against non-literals are not matched by equality. Even though Fraction(1) looks like itâ€™s creating an instance of Fraction, it doesnâ€™t.

I donâ€™t think your problem is due to the int, other than to the extent that int(1) cannot match the value youâ€™re providing, which forces it to check the next option. The same problem comes up without that check:

>>> x = Fraction(1)
>>> match x:
... case Fraction(1): print("Is 1")
... case _: print("Isn't")
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
case Fraction(1): print("Is 1")
~~~~~~~~^^^
TypeError: Fraction() accepts 0 positional sub-patterns (1 given)

The inconsistency youâ€™re seeing here is due to the difference between a literal pattern and a class pattern. The class pattern int(1) does an instance check, but the literal pattern 1 only checks that the value compares equal. So int(1) fails and results in the check against Fraction(1).

Iâ€™m not sure why Fraction is expecting zero positional arguments, but you can do it with keywords:

>>> match x:
... case Fraction(numerator=1, denominator=1): print("is 1")
... case Fraction(numerator=1, denominator=2): print("is half")
... case Fraction(numerator=2, denominator=1): print("is two")
...
is 1

In your leading statement above - ignoring the mind-reading implication - your reference to Fraction(1) was in its occurrence in the case clause, not the input?

I understand that Fraction has no __match_args__, which is why Iâ€™ve implemented it in my custom Fraction subclass.

On this point, I agree, based on this example it would appear so:

case patterns against non-literals are not matched by equality

Perhaps not the best choice then for my implementation. An if statement will do, as I am looking for value equality.

Yes, should have been more clear about that. ^{[1]}

You probably donâ€™t want to do this:

Probably to prevent this exact confusion. Which of the following should be valid? Only one of them can be:

match frac:
case Fraction(1, 2): ...
case Fraction(1/2): ...
case Fraction("1/2"): ...

But if Fraction.__match_args__ was defined, they would all not raise an exception at all. This is an annoying limitation of the current pattern matching syntax. (not that I have a good suggestion for fixing this)

I would say, based on my very limited use of the match-case statement, is that although itâ€™s nice, itâ€™s easy to end up using it to rewrite an if statement where values are being compared for equality. But pattern matching is almost like this, but in a different way.

But if Fraction.__match_args__ was defined, they would all not raise an exception at all. This is an annoying limitation of the current pattern matching syntax. (not that I have a good suggestion for fixing this)

For example, if MyClass.__match_args__ is ("left", "center", "right") that means that case MyClass(x, y) is equivalent to case MyClass(left=x, center=y). Note that the number of arguments in the pattern must be smaller than or equal to the number of elements in match_args; if it is larger, the pattern match attempt will raise a TypeError.

This one, without a shadow of a doubt. What Iâ€™m less sure of is what would happen if someone tried to match with a single integer, and whether that would have sane semantics.

Yes, this means that if one of the latter is chosen, the first doesnâ€™t work. But if the first is chosen, the latter two have surprising semantics (matching only the numerator).

Yep! But that isnâ€™t what OP wanted to do. And if __match_args__ was defined already, this would have silently introduced a bug that OP might not have noticed for a long time. Having to write out numerator and denominator forces you to think about the fact that you are doing component wise comparison. Also see this previous discussion that popped up in related topics: Missing `__match_args__` attribute for `Fraction`

Iâ€™ll have to break my last promise and post again - reading this post I now see the risk in defining __match_args__ in my Fraction subclass. Itâ€™s because a (numerator, denominator) combination is not enough to uniquely identify a fraction because of the reduction to a coprime pair. Although it depends on the form of the match-case statement - it can be done sensibly.

Yeah, I was expecting to respond something like â€śto match against a Fraction, you need both the numerator and the denominatorâ€ť. So thereâ€™s no way to require two args? Thatâ€™s a definite quirk then.

Not currently, although thatâ€™s not a terrible feature request. I donâ€™t know if this was suggested back when PEP 634/5/6 were discussed, but it would probably require a new PEP at this point.