cpython icon indicating copy to clipboard operation
cpython copied to clipboard

[match-case] Allow matching Union types

Open randolf-scholz opened this issue 2 years ago • 9 comments

Feature or enhancement

Since python 3.10, isinstance and issubclass allow testing against Union-types (PEP604). However, the same is not true for match-case.

Pitch

case klass(): should probably be 1:1 equivalent with isinstance(x, klass):.

from typing import TypeAlias

Numeric: TypeAlias = int | float

assert isinstance(1.2, Numeric)  # ✔

match 1.2:
     case Numeric():  # TypeError: called match pattern must be a type
          pass

This carries extra benefits, as TypeAliases are kind of necessary to write legible code when working with large unions, such as e.g. PythonScalar: TypeAlias = None | bool | int | float | complex | bool | str | datetime | timedelta.

Linked PRs

  • gh-118525
  • gh-118644

randolf-scholz avatar Jun 29 '23 14:06 randolf-scholz

This can also play nicely with new type keyword.

>>> type Numberic = int | str
>>> match 1:
...    case Numeric():
...       print(1)
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
TypeError: called match pattern must be a class

Also isinstance() is not supported for this form of TypeAliases, but support for old ones:

>>> isinstance(1, Numeric)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: isinstance() arg 2 must be a type, a tuple of types, or a union

>>> Numeric2 = int | float
>>> isinstance(1, Numeric2)
True

>>> from typing import Union
>>> isinstance(1, Union[int, float])
True

However, there are couple of things that bother me:

  1. Numeric() form, it seems logical for classes, but it feels a bit out of place for type aliases (personal opinion)
  2. Union might contain things like Literal types, so isinstance(1, Literal[1] | float) won't work
  3. What about unions that use string type notations? Like Numeric = Union["SomeForwardRef", int]?

sobolevn avatar Jun 29 '23 14:06 sobolevn

@sobolevn As far as I understand it, isinstance(obj, Union[x, y, z]) is equivalent to isinstance(obj, (x, y, z)) is equivalent to isinstance(obj, x) or isinstance(obj, y) or isinstance(obj, z). So if members of the Union-type are not eligible for isinstance checks (such as e.g. list[int] or Literal), this is not covered.

So there are no surprises here, it is merely a short form.

randolf-scholz avatar Jun 29 '23 15:06 randolf-scholz

A workaround is to use if isinstance.

MyType = str | int

match "some string":
    case a if isinstance(a, MyType):
        print("matched")
    # ... other patterns
    case _:
        print("no match")

schiegl avatar Jan 10 '24 11:01 schiegl

@sobolevn I opened https://github.com/python/cpython/issues/113904 as a separate issue for isinstance calls with PEP 695 syntax.

randolf-scholz avatar Jan 10 '24 11:01 randolf-scholz

This is for 3.14 right? There’s no time for 3.13.

gvanrossum avatar May 03 '24 01:05 gvanrossum

There are some open questions:

  1. should it support subpatterns (without, I think it's a pretty pointless feature)
  2. How does it interact with PEP695 style type aliases.

Current situation:

match node:
    case ast.FunctionDef(name=name, body=body) | ast.AsyncFunctionDef(name=name, body=body):
        ...

What would be nice to have:

FuncDef: TypeAlias = ast.FunctionDef | ast.AsyncFunctionDef

match node:
    case FuncDef(name=name, body=body):
        ...

But with PEP695-style type aliases, if I follow the conclusion of #113904, we would have:

type FuncDef = ast.FunctionDef | ast.AsyncFunctionDef

match node:
    case FuncDef.__value__(name=name, body=body):
       ...

which looks kinda funky. So should the class-pattern be special cased for TypeAliasType? In #113904, it was decided to not special case isinstance/issubclass checks for TypeAliasType, so I'd imagine we won't have it here either.

randolf-scholz avatar May 03 '24 09:05 randolf-scholz

So are you planning to write a PEP about this? Given that the isinstance support was PEP 604, I think it makes sense to have a PEP for this as well. From the discussion above it's hard to extract the feature being proposed, and I don't want to have to learn it from the PR.

gvanrossum avatar May 20 '24 15:05 gvanrossum

Currently, I don't have time for that, but I could write it if required. The feature is simply to allow matching class-patterns, when the class is given by an instance of types.UnionType.

One could also consider making a larger PEP for 3.14 with combining it with other proposed match-case improvements, such as __match__, or matching literal sets.

randolf-scholz avatar May 20 '24 18:05 randolf-scholz

I am not a fan of "omnibus" PEPs. If you don't want to write a PEP, could you at least write up a concise specification for what you are proposing, which can be understood independently from the PR? I would prefer not to have to infer the intended semantics from the submitted code (I hope you understand). Once I understand the proposal I can give you a better recommendation about whether a PEP would be required or not.

gvanrossum avatar May 20 '24 20:05 gvanrossum