nautechsystems/nautilus_trader · error · anyhow::Error

Expected list from decode_record_batch_py

Error message

Expected list from decode_record_batch_py

What it means

After `decode_record_batch_py` returns, the Rust decoder casts the result to a PyList. This error is raised if the Python method returned something other than a list (dict, tuple, generator, None), since the decoder expects a list of decoded custom data instances. It indicates the class's decode implementation violates the expected contract.

Source

Thrown at crates/model/src/python/data/mod.rs:482

            (
                (&raw mut ffi_array as usize),
                (&raw mut ffi_schema as usize),
            ),
        )?;

        let metadata_py = pyo3::types::PyDict::new(py);
        for (k, v) in metadata {
            metadata_py.set_item(k, v)?;
        }

        let py_list = data_class
            .bind(py)
            .call_method1("decode_record_batch_py", (metadata_py, py_batch))
            .map_err(|e| anyhow::anyhow!("Failed to call decode_record_batch_py: {e}"))?;

        let list = py_list
            .cast::<pyo3::types::PyList>()
            .map_err(|_| anyhow::anyhow!("Expected list from decode_record_batch_py"))?;

        let mut result = Vec::new();
        for item in list.iter() {
            let wrapper = PythonCustomDataWrapper::new(py, &item)
                .map_err(|e| anyhow::anyhow!("Failed to create wrapper: {e}"))?;
            result.push(crate::data::Data::Custom(
                crate::data::CustomData::from_arc(Arc::new(wrapper)),
            ));
        }
        Ok(result)
    })
}

/// Registers a custom data **type** (class) with the catalog registry.
///
/// Use this when you prefer to pass the class instead of a sample instance.
/// The class must have:
/// - `type_name_static()` class method or `__name__` (used as type name in storage)

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Make `decode_record_batch_py` return an actual Python `list` of data instances (`return list(decoded)`).
  2. Check what your implementation returns on the empty-batch path — return `[]`, not None.
  3. Wrap the return value in `list(...)` if it is built from a generator or tuple.
  4. Confirm against the nautilus version's decoder contract for custom data.

Example fix

# before
def decode_record_batch_py(cls, metadata, batch):
    return (cls(**row) for row in batch)  # generator, not list

# after
def decode_record_batch_py(cls, metadata, batch):
    return [cls(**row) for row in batch]
Defensive patterns

Strategy: validation

Validate before calling

result = MyClass.decode_record_batch_py(metadata, batch)
assert isinstance(result, list), f"decoder must return list, got {type(result)}"

Type guard

def is_decoded_list(result) -> bool:
    return isinstance(result, list)

Try / catch

try:
    items = decode_record_batch(batch, MyData)
except Exception as e:
    logger.error(f"decoder returned non-list: {e}")
    raise

Prevention

When it happens

Trigger: Decoding a RecordBatch of registered custom data when the class's `decode_record_batch_py` returns a non-list object — e.g. returning a tuple, a generator, a single item instead of a list, or None when no rows decode.

Common situations: Hand-written decoders returning a generator or tuple for convenience; a decoder that returns None on empty input; API change between nautilus versions altering the expected return type.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/f78970542c3842bf. Report an issue: GitHub.