PyBevy is in an early and experimental stage. The API is incomplete, subject to breaking changes without notice, and you should expect bugs. Many features are still under development.
Rust-Bevy Interop (Native Plugin)
Embed Python scripting into an existing Rust Bevy app with PyBevyPlugin and shared components.
Introduction
If you have an existing Rust Bevy project and want to add Python scripting —
for modding support, rapid iteration, or leveraging the Python ecosystem —
PyBevyPlugin lets you embed Python systems directly into your Rust app.
This guide covers:
- Adding
PyBevyPluginto a Rust Bevy app - Writing Python systems that Rust calls each frame
- Defining Rust components accessible from Python (
#[derive(PyComponent)]) - Hot reload for instant feedback
Security Warning:
PyBevyPluginembeds a full CPython interpreter with unrestricted access to the host system. Never execute untrusted Python code.
Quick Start: Adding Python to a Rust App
1. Add PyBevy to your Cargo.toml
[dependencies]
bevy = "0.18"
pybevy = "0.18"2. Create a Python module with your systems
Create a file scripts/game.py:
import math
from pybevy.prelude import *
ROTATION_SPEED = 1.0
@component
class Cube(Component):
pass
def setup(
commands: Commands,
meshes: ResMut[Assets[Mesh]],
materials: ResMut[Assets[StandardMaterial]],
) -> None:
commands.spawn(
Mesh3d(meshes.add(Circle(radius=4.0).mesh())),
MeshMaterial3d(materials.add(StandardMaterial.from_color(Color.WHITE))),
Transform.from_rotation(Quat.from_rotation_x(-math.pi / 2)),
)
commands.spawn(
Cube(),
Mesh3d(meshes.add(Cuboid(1.0, 1.0, 1.0).mesh())),
MeshMaterial3d(materials.add(StandardMaterial.from_color(Color.srgb_u8(124, 144, 255)))),
Transform.from_xyz(0.0, 0.5, 0.0),
)
commands.spawn(
PointLight(shadows_enabled=True),
Transform.from_xyz(4.0, 8.0, 4.0),
)
commands.spawn(
Camera3d(),
Transform.from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3.ZERO, Vec3.Y),
)
def rotate_cube(query: Query[Mut[Transform], With[Cube]], time: Res[Time]) -> None:
for transform in query:
transform.rotation *= Quat.from_rotation_y(time.delta_secs() * ROTATION_SPEED)3. Wire it up in Rust
use pybevy::PyBevyPlugin;
use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(
PyBevyPlugin::new("game")
.with_python_path("scripts")
.with_startup_system("setup")
.with_update_system("rotate_cube")
.with_hot_reload(),
)
.run();
}That's it. Rust runs your Python setup function at startup and rotate_cube every frame.
Sharing Components Across Languages
The real power of native interop is sharing data between Rust and Python.
Use #[derive(PyComponent)] to define a Rust component that Python can query and mutate.
Define the component in Rust
use pybevy::PyComponent;
use bevy::prelude::*;
#[derive(Component, Default, Clone, Debug, PyComponent)]
struct Health {
value: f32,
max: f32,
}The derive macro generates:
PyHealth— Python wrapper with getters/setters forvalueandmaxHealthBridge— bridge struct for registering with PyBevy
Register it with the plugin
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(
PyBevyPlugin::new("game_systems")
.with_python_path("scripts")
.register_component(HealthBridge)
.with_startup_system("setup")
.with_update_system("apply_damage"),
)
.add_systems(Startup, spawn_with_health)
.add_systems(Update, print_health)
.run();Use it from Python
Registered components are injected into pybevy._pybevy:
from pybevy._pybevy import Health # type: ignore[import-not-found]
def apply_damage(query: Query[Mut[Health]], time: Res[Time]) -> None:
for health in query:
health.value -= 5.0 * time.delta_secs()
if health.value < 0.0:
health.value = health.maxRust reads the updated values
fn spawn_with_health(mut commands: Commands) {
commands.spawn((
Health { value: 100.0, max: 100.0 },
Transform::from_xyz(0.0, 2.0, 0.0),
));
}
fn print_health(query: Query<&Health>, mut frame_count: Local<u32>) {
*frame_count += 1;
if *frame_count % 60 == 0 {
for health in &query {
println!("[Rust] Health: {:.1}/{:.1}", health.value, health.max);
}
}
}Rust spawns the entity, Python mutates Health every frame, Rust reads it back — full round-trip.
Mixing Rust and Python Systems
You can freely combine Rust and Python systems in the same app:
fn main() {
App::new()
.add_plugins(DefaultPlugins)
// Rust handles input and rendering setup
.add_systems(Startup, setup_camera)
.add_systems(Update, handle_input)
// Python handles gameplay logic
.add_plugins(
PyBevyPlugin::new("gameplay")
.with_python_path("scripts")
.with_startup_system("spawn_enemies")
.with_update_system("ai_behavior"),
)
// Rust handles cleanup
.add_systems(Last, cleanup_dead_entities)
.run();
}Both Rust and Python systems can read and write the same built-in components
(Transform, PointLight, etc.). Custom components defined in Python via @component
are only accessible from Python; use #[derive(PyComponent)] for cross-language components.
Hot Reload
Enable hot reload with .with_hot_reload() to modify Python scripts while the app is running:
| Key | Action |
|---|---|
| F5 | Full reload — clears entities, re-imports module, re-runs Startup |
| F6 | Toggle default mode between Full and Partial |
| File save | Auto-reload (requires native-hot-reload Cargo feature) |
Try it: Change ROTATION_SPEED = 1.0 to 5.0 in the Python file, save, press F5.
For auto-reload on file save, enable the feature:
[dependencies]
pybevy = { version = "0.18", features = ["native-hot-reload"] }PyBevyPlugin API Reference
| Method | Description |
|---|---|
PyBevyPlugin::new(module) |
Load systems from a Python module |
.with_python_path(path) |
Add directory to Python's sys.path |
.with_startup_system(name) |
Register a Startup system |
.with_update_system(name) |
Register an Update system |
.with_last_system(name) |
Register a Last system |
.with_system(name, stage) |
Register a system on a specific stage |
.register_component(bridge) |
Register a #[derive(PyComponent)] type |
.with_hot_reload() |
Enable hot reload (F5/F6) |
.with_auto_discovery() |
Auto-find startup, update, last functions |
Supported PyComponent Field Types
#[derive(PyComponent)] supports:
- Primitives:
f32,f64,i32,u32,bool, etc. - Types implementing standard
Into/Fromconversions
Use #[py_name("CustomName")] on the struct to override the Python class name.