nautechsystems/nautilus_trader · error

Python on_dispose failed: {e}

Error message

Python on_dispose failed: {e}

What it means

Raised by the Rust `DataActor` bridge when the Python actor's `on_dispose()` callback raises. `dispatch_on_dispose()` propagates the Python exception and it is rewrapped as this anyhow error. Dispose is the final teardown hook after stop, so failures here happen during component destruction.

Source

Thrown at crates/common/src/python/actor.rs:1040

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

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

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

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

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

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

    fn on_save(&self) -> anyhow::Result<IndexMap<String, Vec<u8>>> {
        self.dispatch_on_save()
            .map_err(|e| anyhow::anyhow!("Python on_save failed: {e}"))
    }

    fn on_load(&mut self, state: IndexMap<String, Vec<u8>>) -> anyhow::Result<()> {

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Check the embedded Python traceback for the exact raise site in on_dispose.
  2. Make dispose idempotent: guard closes/deletes with checks or try/except and log instead of raising.
  3. Ensure on_dispose does not depend on state that on_stop already released.
  4. Test full stop->dispose teardown to confirm cleanup is safe to run twice.

Example fix

// before (Python)
def on_dispose(self):
    self.socket.close()

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

Strategy: try-catch

Validate before calling

# ensure teardown targets still exist before dispose
assert self.socket is not None or getattr(self, 'socket', None) is None, 'socket state inconsistent'

Type guard

def closable(res):
    return res is not None and hasattr(res, 'close')

Try / catch

def on_dispose(self):
    try:
        ...  # final teardown
    except Exception as e:
        self.log.error(f"on_dispose failed: {e}")
    finally:
        self.socket = None

Prevention

When it happens

Trigger: Any exception in the user's Python `on_dispose`: freeing resources already freed, double-closing connections, or referencing members cleaned up in on_stop that ran earlier in teardown.

Common situations: Shutdown of a live node where dispose closes a socket already closed in on_stop; deleting temp files that no longer exist; accessing a client set to None during teardown.

Related errors


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