Inconsistency in pattern matching involving `fractions.Fraction` objects

Hi

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.

1 Like

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)


  1. With regards to mind reading: This is an observation based on what behavior you are clearly expecting. Unless you wrote code that doesn’t match what you think is going to happen, which I would describe as a malicious question in a help forum ↩︎

The error says Fraction does not accept any postional sub-patterns - I guess this is because of the lack of __match_args__.

Thanks for the clarification.

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.

Final point on this:

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)

In the docs we have 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.

1 Like