Python bindings¶
Python exposes opaque native handles rather than mirroring Rust state. The built-in handle types are
defined in crates/reinfors-py/src/lib.rs, then given typed and name-addressable Python surfaces in
python/reinfors.
Register a built-in game¶
GridWorld is a complete, compact example. Search its integration points before editing the large binding module:
1. Add the native dispatch¶
In crates/reinfors-py/src/lib.rs:
- Import the game, encoder, event, and reward from
reinfors-games. - Add its validated configuration to
GameSpec, its representation toEncoderSpec, and handle both in the compatibility/default helpers,num_agents, andspaces. - Add it to
game_cfg,reward_schema,build_reward, andRewardBox. - Add concrete arms to
build_engineandbuild_env, including its codec when snapshots are supported. - Include any game-specific fields exposed by common handle methods, such as
truncation_horizon.
The public constructor is a static method on GameHandle. GridWorld's arm validates before storing
the spec and is the pattern to copy:
#[staticmethod]
#[pyo3(signature = (size=5, goal_row=None, goal_col=None, max_ticks=1000, encoder=None))]
#[pyo3(name = "GridWorld")]
fn gridworld(
size: i32,
goal_row: Option<i32>,
goal_col: Option<i32>,
max_ticks: Option<usize>,
encoder: Option<EncoderHandle>,
) -> PyResult<Self> {
check_max_ticks(max_ticks)?;
// Invalid negative sizes must reach validate() as errors, not overflow here.
let corner = size.saturating_sub(1);
let goal = (goal_row.unwrap_or(corner), goal_col.unwrap_or(corner));
GridWorld { size, goal, max_ticks }
.validate()
.map_err(pyo3::exceptions::PyValueError::new_err)?;
game_handle(GameSpec::GridWorld { size, goal, max_ticks }, encoder)
}
Keeping the configuration in GameSpec allows the binding to reconstruct the concrete generic
composition only when an Engine or Env is built. Constructor failures must become Python
exceptions rather than Rust panics. Add the default encoder constructor to EncoderHandle; every
game constructor exposes the same optional encoder= seam even when only one view exists.
Several dispatch details are correctness constraints:
reward_schemais the single source used by both concrete reward construction and resolved configuration; do not repeat reward defaults elsewhere.- Handles store parameters before a game/policy/learner composition is known, so validate composed
parameters in
build_for_game. - Simultaneous joint-space checks use every agent for MCTS/AlphaZero but only the co-movers for expectimax. Changing that exponent can defer an invalid fan to a large mid-collection allocation.
- AlphaZero needs at least two simulations because the first evaluates the root; MCTS accepts one.
MCTS emits only root records, so TreeStrap's
interior_targetsis intentionally ignored there.
2. Add the Python surface¶
Update these four files:
python/reinfors/_reinfors.pyi: add the typedGameHandleandEncoderHandleconstructors;python/reinfors/games.py: export the constructor and add its stable snake-case name to_REGISTRY;python/reinfors/encoders.py: export and register the default encoder;python/reinfors/catalog.py: add game metadata toGAMESand encoder metadata toENCODER_INFO.
The registry supplies both rf.games.GridWorld(...) and config-driven construction through
rf.games.make("gridworld", ...). Its import-time assertion against the catalogue prevents a
registered game from silently disappearing from generated documentation.
3. Verify the installed extension¶
Complete the native rebuild check, then verify the registry entry:
Replace my_game with the stable registry name.
4. Test every public path¶
At minimum, test:
- direct construction and invalid arguments;
- default/explicit encoder equivalence and rejection of an encoder for another game;
rf.games.make(name, **kwargs)andregistered();EngineandEnvcomposition;- resolved-config reconstruction;
- snapshot round trips when a codec is provided;
- Gymnasium or PettingZoo compliance when the game's dynamics fit an adapter.
GridWorld coverage is spread across tests/test_games.py, tests/test_constructor_validation.py,
tests/test_env_snapshots.py, tests/test_gym.py, and tests/test_catalog.py.
Other handle-based components¶
Policies, learners, encoders, chance modes, and noise follow the same overall path:
- add a validated PyO3 factory/spec arm in
crates/reinfors-py/src/lib.rs; - add the typed constructor to
python/reinfors/_reinfors.pyi; - export it from its module under
python/reinfors; - register its stable name in that module's
_REGISTRY; - add catalogue metadata when it is a user-facing algorithm;
- test direct and name-based construction, composition, resolved config, and invalid input.
Use an existing component in the same family as the exemplar. Its Spec occurrences in
crates/reinfors-py/src/lib.rs reveal every type-erased dispatch point that needs an arm.
Solvers are different¶
Solver is an architectural category, not a shared Rust trait or handle registry. The current
Python solvers are concrete PyO3 classes:
- native implementations:
crates/reinfors-core/src/solvers/cfr.rsanddeep_cfr.rs; - binding classes:
PyCfrandPyDeepCfrincrates/reinfors-py/src/lib.rs; - module registration:
_reinforsadds each class withm.add_classnear the end of that file; - typed surface:
CfrandDeepCfrinpython/reinfors/_reinfors.pyi; - public exports: direct aliases in
python/reinfors/solvers.py; - tests:
tests/test_cfr.pyandtests/test_deep_cfr.py.
There is no _REGISTRY, make, or generic solver registration hook in
python/reinfors/solvers.py. Adding a new solver therefore means binding its concrete API, adding it
to the PyO3 module, stub, and public exports, then documenting and testing that API. If the solver
only supports some games, its constructor must validate those compositions and direct users to the
compatibility documentation rather than embed a game list that will become stale.
Binding design rules¶
- Keep Python callbacks at a small number of explicit seams; do not add per-node Python calls.
- Convert callback failures and constructor errors into contextual Python exceptions.
- Validate array rank, shape, dtype, row count, and finite values before native indexing.
- Preserve named batch fields when adding data; positional tuple order is compatibility-only.
- Keep catalogue metadata pure Python so documentation can build without compiling Rust.
- Update type stubs in the same change as runtime bindings.
Update documentation¶
User-facing components also require catalogue metadata. Follow the single catalogue-generation workflow rather than editing generated pages.