nautechsystems/nautilus_trader · error

Python on_dispose failed: {e}

Error message

Python on_dispose failed: {e}

What it means

This error wraps any Python exception raised by the strategy's user-implemented `on_dispose()` callback. The Rust `DataActor::on_dispose` dispatches to the Python instance via `call_method0(py, "on_dispose")` (crates/trading/src/python/strategy.rs:363-368) and re-wraps any `PyErr` as `anyhow::anyhow!("Python on_dispose failed: {e}")`. Disposal is the final teardown of the actor, so a failure here can leak resources or leave the engine unable to cleanly remove the strategy.

Source

Thrown at crates/trading/src/python/strategy.rs:1081

    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. Read the chained Python traceback to find the raising line in `on_dispose`.
  2. Wrap each teardown step in `try/except` so a single failed step doesn't abort the rest of disposal.
  3. Track which resources you actually opened and only release those (or use `contextlib.suppress` / null checks).
  4. Make `on_dispose` idempotent and verify it under the engine's shutdown path in tests.

Example fix

// before (strategy.py)
def on_dispose(self):
    self._file.close()  # raises if file was never opened or already closed

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

Strategy: try-catch

Type guard

def safe_close(resource) -> None:
    if resource is None:
        return
    close = getattr(resource, "close", None)
    if callable(close) and not getattr(resource, "closed", False):
        close()

Try / catch

def on_dispose(self):
    try:
        safe_close(getattr(self, "_file", None))
    except Exception as e:
        self.log.warning(f"dispose cleanup issue: {e}")  # never re-raise in dispose

Prevention

When it happens

Trigger: Calling `strategy.dispose()` (node shutdown, removing the strategy from a trader, or test teardown) when the Python subclass's `on_dispose()` raises: e.g. closing connections already closed, deleting temp files that no longer exist, or an unhandled exception in user teardown code.

Common situations: Interpreter shutdown ordering issues (Python objects half-torn-down when dispose runs), custom resources (files, sockets, threads) whose cleanup assumes they were opened, or dispose being called twice.

Related errors


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