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
- Make `decode_record_batch_py` return an actual Python `list` of data instances (`return list(decoded)`).
- Check what your implementation returns on the empty-batch path — return `[]`, not None.
- Wrap the return value in `list(...)` if it is built from a generator or tuple.
- 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
- Always return a Python list from decode_record_batch_py
- Return [] (not None) on empty batches
- Avoid generators/tuples as decoder return values
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
- Failed to call decode_record_batch_py: {e}
- Failed to convert historical data to Python: unsupported typ
- Python object has no to_json() method or __dict__ attribute
- Instances must have encode_record_batch_py method
- Unknown data type: {type_name}
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/f78970542c3842bf.
Report an issue: GitHub.