A Python ECS framework based on attributes and queries.
5
stars
124
commits
Python
primary language
Aug 19, 2026
updated
punyecs is a tiny Entity Component System (ECS) inspired by tiny-ecs for Python. punyecs operates directly on class attributes as opposed to creating components along with querying mechanisms for fine grain control over which objects are operated on by systems similar to how tiny-ecs works on Lua tables. Decouple what an object can do from what it inherits.
If you use pip:
pip install punyecs
If you use uv:
uv add punyecs
Instead of requiring inheritance, one can specify which attributes to operate on and any object (regardless of class) that has those attributes is operated on. That is, if a Player has an x and y attribute and an (unrelated) Enemy class has an x and y attribute you can have them both influenced by a World object. This avoids complicated inheritance hierarchies.
Here is a small example to illustrate the above:
from punyecs import World, requirements, Trait, give_traits
w = World()
Pos = Trait(x=0.0, y=0.0)
@give_traits(Pos)
class Player:
pass
@give_traits(Pos, override={"x": 1.0, "y": 1.0})
class Enemy:
pass
@requirements(w, Pos)
def move(e, dt):
e.x += 0.1 * dt
e.y += 0.1 * dt
player = Player()
enemy = Enemy()
w.add(player)
w.add(enemy)
w.update(1)
print(player.x) # Prints 0.1
print(player.y) # Prints 0.1
print(enemy.x) # Prints 1.1
print(enemy.y) # Prints 1.1
We may also do exclusions for fine grain control. Returning to the example above, we may want various enemies to move like above but instead want to allow controller input for the player object. We can avoid influencing the player object by using a querying DSL. The function move becomes:
from punyecs import c
# ...
@requirements(w, Pos, subject_to=c.isnot(player))
def move(e, dt):
e.x += 0.1 * dt
e.y += 0.1 * dt
Then after every world.update(1), the player object will still remain at x=0.0, y=0.0.
https://github.com/user-attachments/assets/682ea45e-18a1-4d3e-9176-59686a80a524
Check out the examples folder or read a step-by-step guide on how to make it!
Esper is a well-established, performance-focused ECS for Python, and a reasonable default choice for most games. punyecs takes a different set of trade-offs — here's how they differ in practice:
subject_to, and you're building something small-to-medium where Esper's performance headroom isn't the deciding factor.For a more detailed comparison see this section of the documentation.
Even more filtering options are available. To learn more, see the readthedocs.
Feel free to file an issue, make a pull request or, if you really like the project, donate. (No pressure though.)
124 commits
Python
100.0%
A Python ECS framework based on attributes and queries.
5
stars
124
commits
Python
primary language
Aug 19, 2026
updated
punyecs is a tiny Entity Component System (ECS) inspired by tiny-ecs for Python. punyecs operates directly on class attributes as opposed to creating components along with querying mechanisms for fine grain control over which objects are operated on by systems similar to how tiny-ecs works on Lua tables. Decouple what an object can do from what it inherits.
If you use pip:
pip install punyecs
If you use uv:
uv add punyecs
Instead of requiring inheritance, one can specify which attributes to operate on and any object (regardless of class) that has those attributes is operated on. That is, if a Player has an x and y attribute and an (unrelated) Enemy class has an x and y attribute you can have them both influenced by a World object. This avoids complicated inheritance hierarchies.
Here is a small example to illustrate the above:
from punyecs import World, requirements, Trait, give_traits
w = World()
Pos = Trait(x=0.0, y=0.0)
@give_traits(Pos)
class Player:
pass
@give_traits(Pos, override={"x": 1.0, "y": 1.0})
class Enemy:
pass
@requirements(w, Pos)
def move(e, dt):
e.x += 0.1 * dt
e.y += 0.1 * dt
player = Player()
enemy = Enemy()
w.add(player)
w.add(enemy)
w.update(1)
print(player.x) # Prints 0.1
print(player.y) # Prints 0.1
print(enemy.x) # Prints 1.1
print(enemy.y) # Prints 1.1
We may also do exclusions for fine grain control. Returning to the example above, we may want various enemies to move like above but instead want to allow controller input for the player object. We can avoid influencing the player object by using a querying DSL. The function move becomes:
from punyecs import c
# ...
@requirements(w, Pos, subject_to=c.isnot(player))
def move(e, dt):
e.x += 0.1 * dt
e.y += 0.1 * dt
Then after every world.update(1), the player object will still remain at x=0.0, y=0.0.
https://github.com/user-attachments/assets/682ea45e-18a1-4d3e-9176-59686a80a524
Check out the examples folder or read a step-by-step guide on how to make it!
Esper is a well-established, performance-focused ECS for Python, and a reasonable default choice for most games. punyecs takes a different set of trade-offs — here's how they differ in practice:
subject_to, and you're building something small-to-medium where Esper's performance headroom isn't the deciding factor.For a more detailed comparison see this section of the documentation.
Even more filtering options are available. To learn more, see the readthedocs.
Feel free to file an issue, make a pull request or, if you really like the project, donate. (No pressure though.)
124 commits
Python
100.0%