Right, it probably makes sense to make this less strict. The following seems pretty safe:
from typing import NamedTuple, TypeAlias
class A(NamedTuple):
x: int # only `x` this time
class B(NamedTuple):
x: int
y: str
AB: TypeAlias = A | B
match ab:
case AB(x): # only `x`
print(f"{x=}")
as syntactic sugar for
match ab:
case A(x) | B(x, _): # note the wildcard usage
print(f"{x=}")
which would work as long as all the __match_args__
start with an identical sequence.
And then you could loosen it even further by allowing different names in the __match_args__
:
class A(NamedTuple):
x: int
class B(NamedTuple):
x: int
AB: TypeAlias = A | B
match ab:
case AB(x):
print(f"{x=}")
That’s probably also fine, though it could lead to hard-to-notice bugs.
I’m not sure I’m understanding correctly, but to re-iterate, the main motivation is as an alternative to the sealed class proposal; as an alternative way of making static exhaustiveness checks work for subclasses.
We can of course define it like this:
@dataclass
class AB: ...
@dataclass
class A(AB):
x: int
@dataclass
class B(AB):
x: int
ab: AB = A(3)
match ab:
case AB(x):
print(f"{x=}")
This works, but then when we want to distinguish A
and B
, static type checkers will complain:
match ab: # match is not exhaustive
case A(x):
print("this is A")
case B(x):
print("this is B")
So, we would like to use the type A | B
, so that exhaustiveness checking works, but when we do that and use a type alias of A | B
everywhere, we encounter the problem that you can’t use that type alias as a case:
, which is something you might sometimes want to do.
class ExpressionBase:
def shared_functionality(self): ...
@dataclass
class Name(ExpressionBase):
name: str
@dataclass
class Operation(ExpressionBase):
left: "Expression"
op: str
right: "Expression"
Expression: TypeAlias = Name | Operation
def f(node: Expression | Statement):
match node:
case Expression(): # we want to match on any `Expression` here
print("it's an expression")
case Statement():
print("it's a statement")
def g(expr: Expression):
match expr:
case Name(name):
print(f"{name=}")
case Operation(left, op, right):
print("it's an operation")
Hmm, typing out this example, it seems that it’s in practice probably most useful to use the union type alias in a case
that does not bind any variables.
So, we should definitely do the thing where it’s fine when all the __match_args__
aren’t perfectly identical.