PyO3/pyo3 · error

Empty enum

Error message

Empty enum

What it means

The #[pyclass] macro computes the output type of an enum by unioning the output types of all its variants with Iterator::reduce, which returns None for an empty iterator; the expect then panics. It guards the assumption that a pyclass enum has at least one variant. An empty enum (enum E {}) has no representable value, so PyO3 refuses to generate code for it.

Source

Thrown at pyo3-macros-backend/src/intopyobject.rs:475

                target: quote!(#pyo3_path::types::PyAny),
                output: quote!(#pyo3_path::Bound<'py, <Self as #pyo3_path::conversion::IntoPyObject<'py>>::Target>),
                error: quote!(#pyo3_path::PyErr),
            },
            body: quote! {
                match self {
                    #variants
                }
            },
        }
    }

    #[cfg(feature = "experimental-inspect")]
    fn output_type(&self) -> PyExpr {
        self.variants
            .iter()
            .map(|var| var.output_type())
            .reduce(PyExpr::union)
            .expect("Empty enum")
    }
}

// if there is a `'py` lifetime, we treat it as the `Python<'py>` lifetime
fn verify_and_get_lifetime(generics: &syn::Generics) -> Option<&syn::LifetimeParam> {
    let mut lifetimes = generics.lifetimes();
    lifetimes.find(|l| l.lifetime.ident == "py")
}

pub fn build_derive_into_pyobject<const REF: bool>(tokens: &DeriveInput) -> Result<TokenStream> {
    let options = ContainerAttributes::from_attrs(&tokens.attrs)?;
    let ctx = &Ctx::new(&options.krate, None);
    let Ctx { pyo3_path, .. } = &ctx;

    let (_, ty_generics, _) = tokens.generics.split_for_impl();
    let mut trait_generics = tokens.generics.clone();
    if REF {
        trait_generics.params.push(parse_quote!('_a));

View on GitHub (pinned to ac9b6899d3)

Solutions

  1. Add at least one variant to the enum.
  2. If the enum is a never type, do not expose it to Python; keep it out of #[pyclass].
  3. Check feature flags: ensure at least one variant is not compiled out by cfg when the crate builds.
  4. Wrap the type differently (e.g. expose a unit variant placeholder) if a placeholder value is acceptable.

Example fix

// before
#[pyclass]
enum Never {}
// after
#[pyclass]
enum Never {
    Unreachable,
}
Defensive patterns

Strategy: validation

Validate before calling

// Before adding #[pyclass] to an enum, ensure it has variants
assert!(variants > 0, "pyclass enum must have at least one variant");

Type guard

fn has_variants(e: &syn::ItemEnum) -> bool { !e.variants.is_empty() }

Prevention

When it happens

Trigger: Applying #[pyclass] (or the IntoPyObject/derive machinery) to an enum with zero variants, e.g. `#[pyclass] enum Never {}`.

Common situations: Users porting never-type-like empty enums from Rust-only code into pyo3-exposed types, or code-generated enums that ended up with no variants after cfg-stripping (e.g. all variants behind #[cfg] features that are off).

Related errors


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