NautilusTrader Python Runtime, Async Execution, and API Boundaries
Summary
The guide explains how NautilusTrader’s Python package works with its Rust core through PyO3. Python handles application configuration, user components, analysis, and integrations, while Rust owns core runtime state, engines, routing, accounting, and adapters. It describes supported Python components, the synchronous nature of callbacks, and why blocking I/O or lengthy calculations can delay market-data and order processing.
It also compares blocking and asynchronous LiveNode execution, covering hosted asyncio or ASGI loops, shutdown and cancellation, resource disposal, and runtime ownership. The public API boundary is defined by generated type stubs and documented interfaces; undocumented runtime attributes are not guaranteed. The guide cautions that concurrent nodes in one process are unsupported and details lifecycle and adapter support constraints. These are system operation and development guidelines, not a trading strategy or evidence of trading performance.
Key ideas
- Python configures NautilusTrader while Rust owns core engines and mutable runtime state.
- User callbacks run synchronously on the event-processing thread and should return promptly.
- LiveNode supports blocking execution and hosting on a Python asyncio loop, with coordinated shutdown.
- Generated stubs and documented interfaces define the supported Python API.
- Concurrent backtest or live nodes in one process are not supported.
Tags
Full text
# Python # Python NautilusTrader provides a Python control surface over the Rust core through PyO3. Use this guide to understand which runtime owns each part of the system, where Python code executes, and which Python interfaces form the supported public contract. For native Rust applications, see [Rust](rust.md). For installation and supported Python versions, see [Installation](../getting_started/installation.md). ## Runtime model The Python package combines Python facades under `nautilus_trader` with the compiled `nautilus_trader._libnautilus` extension. Prebuilt wheels contain the extension and do not require a Rust toolchain at runtime. | Layer | Responsibility | | ------------------ | ---------------------------------------------------------------------------------------------- | | Python application | Configuration, composition, user components, analysis, and integration with Python services. | | PyO3 bindings | Type conversion, argument validation, exceptions, and ownership-safe wrappers over Rust state. | | Rust core | Domain types, engines, nodes, cache, portfolio, message bus, adapters, and persistence. | Python objects such as `Cache` and `Portfolio` are wrappers over Rust-owned state. Nodes and engines keep their internal runtime objects private and expose bounded inspection and control methods. This preserves one source of state while allowing Python code to configure the system and inspect its results. ## User components Python user components subclass the public PyO3 base classes and override their documented callbacks: | Component | Use | | -------------------- | ----------------------------------------------------------------------------- | | `DataActor` | Subscribe to data, handle events, and run non-trading workflows. | | `Strategy` | Implement trading decisions and submit orders. | | `ExecutionAlgorithm` | Split or schedule routed orders through the execution engine. | | `Controller` | Create and manage actors and strategies through `ImportableControllerConfig`. | Application code constructs configs, registers official adapter factories, and adds components to `BacktestNode`, `BacktestEngine`, or `LiveNode`. Rust remains responsible for routing, engine state, order management, accounting, and venue clients. Callbacks execute **synchronously** on the event-processing thread and must return promptly. Blocking I/O, model inference, or long calculations delay market-data handling and order execution. Offload that work to an executor or another process. See [Configure a live trading node](../how_to/configure_live_trading.md) for the live-trading rule. ## Async execution Rust adapter networking runs on Tokio. Python async libraries run on an asyncio event loop; PyO3 does not turn Python coroutines into Tokio tasks. `LiveNode` supports two execution modes: | Method | Execution context | Signal owner | Completion | | ------------- | ---------------------- | ---------------- | ----------------------------------------------------------- | | `run()` | Calling thread; blocks | `LiveNode` | Returns after coordinated shutdown finishes. | | `run_async()` | Python host loop | Host application | Resolves after the same coordinated shutdown path finishes. | `run_async()` lets an asyncio or ASGI application host the node on its existing loop. It drives the same Rust lifecycle as `run()` and leaves `SIGINT` and `SIGTERM` handling to the host. Compatibility is tested with the default asyncio loop, uvloop, and an ASGI lifespan managed by Uvicorn. The Python wheel does not install uvloop or Uvicorn; applications supply their chosen loop and server. An ASGI application whose lifespan constructs a node must run with one worker and without hot reload. Capture `node.cache`, `node.portfolio`, and `node.handle()` before starting `run_async()`. The coroutine owns the node until it finishes, while the captured objects remain usable. Stop a hosted run through `LiveNodeHandle.stop()`, then await the run task for complete shutdown. Cancellation requests the same graceful shutdown before it propagates. Call `node.dispose()` after the task finishes to release the node's resources. A host must wait for the handle to report `Running` before reporting startup complete, supervise the run task for its lifetime, and fail the service if the task completes unexpectedly. See [Hosted event loops](live.md#hosted-event-loops) for the lifecycle, cancellation, fairness, and cache-backing contract. ## Public API contract The generated type stubs under `python/nautilus_trader/` define the supported Rust-bound Python surface. They record public classes, methods, properties, parameters, and return types from the Rust binding sources. The [Python API reference](../api_reference/index.md) renders the same public modules and their documentation. A runtime attribute on a Rust-bound class absent from the generated stubs is not part of the supported contract. The documented Python client, provider, and importable-config classes also form a public interface. PyO3 validates bound arguments before Rust code runs and maps fallible operations to Python exceptions. Code should handle the documented exception type instead of depending on an internal Rust error representation. Generated stubs are source-derived artifacts. Binding changes update the Rust source and regenerate the stubs; the checked-in `.pyi` files are not independent API definitions. :::warning[Side enum compatibility aliases] `OrderSide.NO_ORDER_SIDE` and `PositionSide.NO_POSITION_SIDE` remain available as compatibility aliases for `None`. They are not enum members and may be removed in a future version. Use `None` for optional side values. ::: ## Ownership and lifecycle Rust ownership remains visible at node boundaries: - `BacktestNode` keeps its engines internal. Preserve an engine after a run with `dispose_on_completion=False`, then inspect it through the node's cache, portfolio, statistics, and report methods. - `LiveNode.run_async()` lends the node to its coroutine. State access through the node raises during the run, while `is_running` and `handle()` remain available. A `dispose()` call during the run is a no-op, not a deferred request. Call it again after the run finishes; objects captured before the run remain available until then. - Concurrent `LiveNode` or `BacktestNode` instances in one process are not supported because their runtime state is not isolated. Dispose one node before starting the next, or use separate processes for parallel execution. Before constructing a replacement `LiveNode` on the same thread, also release all references to the previous node. With a single reference, use `del node` before the next build; reassigning the variable constructs the replacement before releasing the old node. These boundaries prevent Python references from exposing mutable engine internals or creating multiple owners for the same runtime state. ## Support boundaries Official adapters are implemented in Rust and exposed through Python configs, factories, clients, and data types under `nautilus_trader.adapters`. Their integration guides define the supported venue capabilities. Custom live adapters subclass the Python client bases and register through `LiveNodeBuilder` or `LiveNode.build`. Their async work runs on the node's bound Python event loop. They receive a read-only cache view and emit typed data, events, and reports through queued output. Independent Rust/PyO3 packages can use the same Python protocol with model objects from the installed wheel. See the [Python adapter interface](../developer_guide/python_adapters.md) for the supported hooks, factory/config contract, lifecycle rules, and v1 migration limits. ## Choosing Python or Rust Use Python when application composition, rapid strategy development, analysis tools, or integration with the Python ecosystem matters. Use Rust when the application must run without a Python runtime or needs native traits and direct crate-level control. Both paths use the same Rust domain model and engines. The [Rust capability matrix](rust.md#capability-matrix) shows which components and official adapters are exposed through each path. ## Related guides - [Architecture](architecture.md) - Core components, threading, and dependency flow. - [Rust](rust.md) - Native Rust APIs and runtime use. - [Live trading](live.md) - LiveNode lifecycle and hosted event loops. - [Backtesting](backtesting/) - Backtest engines, nodes, data, and venues. - [Adapters](adapters.md) - Official adapter configuration and routing. - [Migration from v1](../../MIGRATION_V2.md) - Python API changes and migration boundaries.
Shown in full with attribution under the source's licence. Licence: LGPL-3.0
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.