nautechsystems/nautilus_trader · error

Python on_dispose failed: {e}

Error message

Python on_dispose failed: {e}

What it means

PyExecutionAlgorithm's DataActor on_dispose dispatches the Python `on_dispose` callback directly (no Rust pre-step); any Python exception is re-wrapped with this anyhow message. Dispose releases the component's resources, so this error means cleanup code failed in Python.

Source

Thrown at crates/trading/src/python/algorithm.rs:570

        self.dispatch_no_args("on_stop")
            .map_err(|e| anyhow::anyhow!("Python on_stop failed: {e}"))
    }

    fn on_resume(&mut self) -> anyhow::Result<()> {
        ExecutionAlgorithm::on_resume(self)?;
        self.dispatch_no_args("on_resume")
            .map_err(|e| anyhow::anyhow!("Python on_resume failed: {e}"))
    }

    fn on_reset(&mut self) -> anyhow::Result<()> {
        ExecutionAlgorithm::on_reset(self)?;
        self.dispatch_no_args("on_reset")
            .map_err(|e| anyhow::anyhow!("Python on_reset failed: {e}"))
    }

    fn on_dispose(&mut self) -> anyhow::Result<()> {
        self.dispatch_no_args("on_dispose")
            .map_err(|e| anyhow::anyhow!("Python on_dispose failed: {e}"))
    }

    fn on_degrade(&mut self) -> anyhow::Result<()> {
        self.dispatch_no_args("on_degrade")
            .map_err(|e| anyhow::anyhow!("Python on_degrade failed: {e}"))
    }

    fn on_fault(&mut self) -> anyhow::Result<()> {
        self.dispatch_no_args("on_fault")
            .map_err(|e| anyhow::anyhow!("Python on_fault failed: {e}"))
    }

    fn on_time_event(&mut self, event: &TimeEvent) -> anyhow::Result<()> {
        ExecutionAlgorithm::on_time_event(self, event)?;
        self.dispatch_time_event(event)
            .map_err(|e| anyhow::anyhow!("Python on_time_event failed: {e}"))
    }

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Read the chained `{e}` traceback and fix the Python on_dispose implementation.
  2. Make dispose idempotent (set released handles to None and check before use).
  3. Ensure dispose is called at most once per lifecycle; track a disposed flag if needed.
  4. Verify the zero-arg `def on_dispose(self)` signature.

Example fix

// before
def on_dispose(self):
    self.cache.close()  # second call raises

// after
def on_dispose(self):
    if self.cache is not None:
        self.cache.close()
        self.cache = None
Defensive patterns

Strategy: try-catch

Validate before calling

# Python: guard against double dispose
if getattr(self, "disposed", False): return

Try / catch

if let Err(e) = actor.on_dispose() {
    log::error!("python on_dispose failed: {e:#}"); // release handles regardless
}

Prevention

When it happens

Trigger: Calling on_dispose when the Python on_dispose raises — e.g. freeing resources twice, or bad attribute access during teardown.

Common situations: Double dispose when both stop and dispose paths run cleanup; dropping references already released; exceptions in __del__-like cleanup logic.

Related errors


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