Skip to content

Making a pyclass enum recursive breaks automatic implementation of Clone/FromPyObject #5510

@bbannier

Description

@bbannier

Bug Description

Rust supports recursive enums if they have an indirection, e.g., the following is supported by Rust

enum E {
    A(i64),
    B(String),
    C(Vec<E>),
}

This shows up for me in a custom serialization format, but JSON would have the same use case.

Separately, when PyO3 sees an enum decorated with #[pyclass] which derives Clone it will automatically implement FromPyObject which allows converting from Python types which is especially useful for huge Rust enums. This seems to be slightly broken for enums, but it seems one can also derive FromPyObject which will automatically implement Clone, e.g.,

#[derive(FromPyObject, Eq, PartialEq)]
#[pyclass(eq)]
enum E {
    A(i64),
    B(String),
    // C(Vec<E>), // HERE.
}

#[pyfunction]
fn make_e(e: E) -> E {
    e
}

generates a factory function for E which can be used in Python to convert from supported types, e.g.,

from my_crate import E, make_e

assert make_e(1) == E.A(1)
assert make_e("2") == E.B("2")

If I want to add variant C which is commented out above, the existing derives do not work anymore and the code is rejected at compile time with

   --> src/lib.rs:15:7
     |
  15 |     C(Vec<E>), // HERE.
     |       ^^^^^^ the trait `Clone` is not implemented for `E`
     |
     = note: required for `Vec<E>` to implement `Clone`
     = note: required for `Vec<E>` to implement `PyO3GetField<'_>`
note: required by a bound in `ConvertField::<false, IMPLEMENTS_INTOPYOBJECT>::convert_field`

If I change the derive to #[derive(Clone, Eq, PartialEq)] the code compiles again, but the generated FromPyObject implementation seems broken

$ python bla.py
  File "bla.py", line 3, in <module>
    assert make_e(1) == E.A(1)
TypeError: argument 'e': 'int' object cannot be converted to 'E'

I however cannot implement it myself since an implementation already exist, so the current macro seems fundamentally broken and the only workaround might be to implement PyClass, PyTypeInfo, FromPyObject, and IntoPyObject by hand.

Steps to Reproduce

See above.

Backtrace

Your operating system and version

macos-15.6.1

Your Python version (python --version)

Python 3.9.20

Your Rust version (rustc --version)

rustc 1.90.0 (1159e78c4 2025-09-14)

Your PyO3 version

0.26.0

How did you install python? Did you use a virtualenv?

via rye and requires-python = ">=3.9" in my pyproject.toml

Additional Info

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions