PyO3 / PyO3/pyo3

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

Open
#5,510 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
16.2k
Forks
1k
Avg merge
2d 6h
Merged PRs (30d)
66

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the reproducer in src/lib.rs, focusing on the #[pyclass] enum, its Clone or FromPyObject derives, and the recursive C(Vec) variant. Run the shown Python make_e calls and verify that recursive enums still compile and convert supported Python values correctly; the issue does not name a specific test file.

Written by the indexing model from the issue text.

Assessment

Tech stack
python, rust
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.