Syntactic sugar for union cases in match statements

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.