ADTs for PyO3

Summary

PyO3 is a solution for seamlessly integrating Rust and Python. It is similar in spirit to pybind, which is for C++ and Python.

I have contributed a fairly large change to PyO3, adding support for Rust enums that are "complex" in the sense that their variants may have payloads.

This change enables PyO3 to support the full range of ADTs (Algebraic Data Types) in Rust and expose them to Python.

The PR is here: Full ADT support with pyclass for complex enums #3582.

Details

First, a little bit of context.

C/C++ enums and Python Enums are "simple"; they are collections of symbols that do not have internal structure.

Rust enums, on the other hand, are an extension of those ideas. A Rust enum may be "complex"; its symbols (called variants) may have internal structure.

Because Python simply does not directly support the kind of "payload-carrying" enums that Rust does, it was a difficult undertaking to connect the two languags.

The key idea of my solution is to expose each Rust enum to Python as entities that form a class hierarchy.

However, there are several complicating factors that a practical solution needs to deal with. For example, similar to Rust match expressions, Python has match statements. The generated Python machinery needs to be compatible with that, and work in an intuitive way.

Long story short, I've opened the PR on 2023 November 17, and it was merged on 2024 January 19, after 2 months of experimentation, brainstorming with the maintainers, banging my head against a wall keyboard at times, and getting all the little details working.

Example

Imagine that you have this definition on the Rust side:

#[pyclass]
enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
    RegularPolygon { side_count: u32, radius: f64 },
    Nothing { },
}

By having the #[pyclass] annotation on the type, it can be exposed to Python. In other words, you can include it in a library called geom, compile that to a geom.so file via some PyO3 magic, import it in Python, and use the type.

Something like this:

from geom import Shape

def count_vertices(shape):
    match shape:
        case Shape.Circle():
            return 0
        case Shape.Rectangle():
            return 4
        case Shape.RegularPolygon(side_count=n):
            return n
        case Shape.Nothing():
            return 0

circle = Shape.Circle(radius=10)
assert count_vertices(circle) == 0

square = Shape::RegularPolygon(side_count=4, radius=10.0)
assert count_vertices(square) == 4