PyO3/pyo3 · error

complex enum has a non-unit variant

Error message

complex enum has a non-unit variant

What it means

PyClassComplexEnum::new is only called after the enum has been determined to be a 'complex' enum (one containing at least one non-unit variant). It uses find + expect to grab the first non-unit variant as a witness, so the panic fires if this constructor is invoked on an enum whose variants are all unit variants — a caller-side invariant violation.

Source

Thrown at pyo3-macros-backend/src/pyclass.rs:676

            ident,
            repr_type,
            variants,
        })
    }
}

struct PyClassComplexEnum<'a> {
    ident: &'a syn::Ident,
    variants: Vec<PyClassEnumVariant<'a>>,
}

impl<'a> PyClassComplexEnum<'a> {
    fn new(enum_: &'a mut syn::ItemEnum) -> syn::Result<Self> {
        let witness = enum_
            .variants
            .iter()
            .find(|variant| !matches!(variant.fields, syn::Fields::Unit))
            .expect("complex enum has a non-unit variant")
            .ident
            .to_owned();

        let extract_variant_data =
            |variant: &'a mut syn::Variant| -> syn::Result<PyClassEnumVariant<'a>> {
                use syn::Fields;
                let ident = &variant.ident;
                let options = EnumVariantPyO3Options::take_pyo3_options(&mut variant.attrs)?;

                let variant = match &variant.fields {
                    Fields::Unit => {
                        bail_spanned!(variant.span() => format!(
                            "Unit variant `{ident}` is not yet supported in a complex enum\n\
                            = help: change to an empty tuple variant instead: `{ident}()`\n\
                            = note: the enum is complex because of non-unit variant `{witness}`",
                            ident=ident, witness=witness))
                    }
                    Fields::Named(fields) => {

View on GitHub (pinned to ac9b6899d3)

Solutions

  1. Route unit-only enums to the simple-enum constructor instead of PyClassComplexEnum::new.
  2. Check the caller's classification logic (e.g. the variants.any(!unit) check) before choosing the complex path.
  3. Report to pyo3 if stock #[pyclass] triggers it on a mixed enum.

Example fix

// before (caller dispatch)
PyClassComplexEnum::new(&mut item_enum) // for enum { A, B }
// after
if has_non_unit_variant { PyClassComplexEnum::new(...) } else { PyClassSimpleEnum::new(...) }
Defensive patterns

Strategy: validation

Validate before calling

// Dispatch correctly: only complex enums go to PyClassComplexEnum
let is_complex = item_enum.variants.iter().any(|v| !matches!(v.fields, syn::Fields::Unit));

Type guard

fn is_complex_enum(e: &syn::ItemEnum) -> bool {
    e.variants.iter().any(|v| !matches!(v.fields, syn::Fields::Unit))
}

Prevention

When it happens

Trigger: Internal misuse: calling PyClassComplexEnum::new on an enum with only unit variants (the simple-enum path should have been taken instead).

Common situations: Only when forking/patching pyo3-macros-backend or routing ASTs to the wrong enum representation (e.g. after changing how #[pyclass(enum)] discriminates simple vs complex enums); invisible to regular users.

Related errors


AI-assisted analysis of PyO3/pyo3@ac9b6899d3 (2026-09-05). Data as JSON: /api/errors/bdbe1b9209c0423b. Report an issue: GitHub.