Encapsulation of variants in `Union`s

Hi all,

I’m a big fan of sum-types and pattern matching. I’m wondering how to properly “namespace” variants in a Union?

Here’s an example. This is how I do sum types today, in a way that works well with type checkers and the match statement:

from typing import Union
from dataclasses import dataclass

@dataclass
class Square:
    center: tuple[float, float]
    length: float
    

@dataclass
class Circle:
    center: tuple[float, float]
    radius: float

type Shape = Union[Square, Circle]

The issue is that Square is not “encapsulated” (for the lack of a better term) to Shape. This creates problems if in the same file, I have

@dataclass
class Square:
     account_token: str
     handle: ...

@dataclass
class Stripe:
    ...

type PaymentEndpoint = Union[Square, Stripe]

The second Square shadows the first Square, which means the following would not properly:

def draw(s: Shape):
    match s:
        case Square(center, length):  # uh oh! probably not what I had in mind
            ...

Contrast this with Rust, where the names are encapsulated to the containing enum:

enum Shape {
  Square {center: (f64, f64), length: f64},
  Circle {center: (f64, f64), radius: f64}
}

enum PaymentEndpoint {
  Square { token: String, handle: ...},
  Stripe { ... }
}

May I ask if there is a recommended construct in Python which does something to what Rust may achieve?

Thanks!

You can use the usual Python constructs for namespacing:

  • put them in different files
  • encapsulate them in a dummy class
class Shape:
    @dataclass
    class Square:
        center: tuple[float, float]
        length: float

    @dataclass
    class Circle:
        center: tuple[float, float]
        radius: float

    type AnyShape = Square | Circle

Sum types and union types are slightly different so I don’t think this can be 1:1, but maybe pretty close in practice.

I wonder if we ever get intersection types it’d be possible to stick Square and Circle as attributes onto a Shape union.

1 Like

What you’re describing is what’s called the sum type in type theory, which is a kind of algebraic data type. It’s also known as the tagged union, disjoint union, and discriminated union. The “sum” refers to the summing of the amount of distinct values that can be assigned to each of the types.

But Python does not have these, or at least, not in the way that would solve your problem. The enum.Enum type is an enumerated type, which is a special case of a sum type. But they’re not nearly as powerful as Rust’s enum.

1 Like

Thanks Tin and Joren - that’s helpful.

I wonder if there are any plans (e.g. existing PEP’s) for Python to gain sum types in the future? I feel like dataclass and typing.Union is almost there…

1 Like