[match-case] Allow matching Union types
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
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:
-
Numeric()form, it seems logical for classes, but it feels a bit out of place for type aliases (personal opinion) -
Unionmight contain things likeLiteraltypes, soisinstance(1, Literal[1] | float)won't work - What about unions that use string type notations? Like
Numeric = Union["SomeForwardRef", int]?
@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.
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")
@sobolevn I opened https://github.com/python/cpython/issues/113904 as a separate issue for isinstance calls with PEP 695 syntax.
This is for 3.14 right? There’s no time for 3.13.
There are some open questions:
- should it support subpatterns (without, I think it's a pretty pointless feature)
- 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.
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.
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.
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.