Open-source project
benmoran56/esper avatar
benmoran56/esper

esper is a pure Python ECS for games and simulations

An ECS (Entity Component System) for Python

703 stars82 forksPythonMIT

At a glance

What is it?
esper is an MIT licensed Entity Component System written entirely in pure Python with no dependencies, where entities are integer ids, components are plain classes or dataclasses, and processors are the only place game logic lives. It targets performance without giving up the standard library, and it is a rewrite rather than a port of other Python ECS libraries.
Who is it for?
esper suits a Python developer prototyping a roguelike or simulation who wants the ECS pattern without a native dependency or a compiled extension, since it is 100% pure Python and drops into a project as a single folder.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 33 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What esper is, and the change in version 3

esper is a lightweight Entity System module for Python, with a focus on performance, and it is MIT licensed. The design is based on the Entity System concepts originally popularized by Adam Martin and others, and the primary focus is to maximize performance while handling most common use cases.

There is a warning at the top of the README about the version 3.0 breaking changes. Version 3.0 removes the World object and migrates its methods to module level functions, and multiple contexts can be created and switched between. The v2.x README is kept on a v2_maintenance branch, so the older API is still documented rather than deleted.

That change is more than a rename. A World object was a place to hang state, and turning it into module level functions with a current context means the same module can host several independent simulations, and switching between them is a single call rather than passing a world around. The README says advanced users can switch contexts, which can be useful for isolating different game scenes that have different Processor requirements.

The package is published on PyPI as esper, the API documentation is on ReadTheDocs, and the author is Benjamin Moran. The release history gives the versions their own names, with v3.7 (Tritoch) on 2026-01-30, v3.6 (Terrato) on 2026-01-24 and v3.3 (Starlet) on 2024-05-28, and the last push was on 2026-09-20.

Installation: a folder, or a pip package

esper is a pure Python package with no dependencies, so there are several ways to install it, and the README says so directly.

You can simply copy the esper folder right into your project and import esper. There is no build step and nothing to compile, which is the practical advantage of a pure Python implementation over the many ECS libraries that ship a C extension.

The other route is PyPI:

bash
    pip install --user --upgrade esper

Or from the source directory:

bash
    pip install . --user

The three ways of using it are a fair summary of the design intent. Copying the folder is what you do when you want to vendor the library and be certain of the version, which is common for a project that is a game jam entry or a tool you distribute. The PyPI route is what you do for a normal application, and the --upgrade flag keeps it current.

The compatibility section says esper attempts to target all currently supported Python releases, any Python version that is not EOL, and that automated testing is currently done for both CPython and PyPy3. The pyproject.toml requires Python 3.8 or later and builds with flit_core, and the module is typed, with a .mypy.ini at the repository root and a .ruff.toml for linting.

Because the whole library is one folder with no dependencies, vendoring it is trivially safe. There is no version of another package to conflict with.

Entities, components and the rule that keeps them dumb

The three core concepts are described with unusual clarity for an ECS library, and the rules are what make the pattern work.

Entities are defined internally as plain integer IDs, 1, 2, 3, 4 and so on. The README says you should not need to care about the individual entity IDs, since entities are queried based on their specific combination of Components and not by their ID. An Entity can be thought of as a specific combination of Components, and creation is done with the esper.create_entity() function, where you can pass Component instances on creation or add and remove them later.

Components are defined as simple Python classes, and the rule is that in keeping with a pure Entity System design philosophy, Components should not contain any processing logic. They may contain initialization logic, and you can use properties to simplify data lookup. The key point is that game logic does not belong in these classes, and Components should have no knowledge of other Components or Entities.

That last constraint is the one that trips people up when they arrive from an object-oriented background, and it is the reason the library is fast. If a component cannot reference another component, then a query for a set of components can be a simple type-indexed lookup rather than a graph traversal.

A component can be a plain class:

python
    class Position:
        def __init__(self, x=0.0, y=0.0):
            self.x = x
            self.y = y

Or, as the README recommends, a dataclass, which removes the repeated attribute names and still allows positional or keyword instantiation:

python
    from dataclasses import dataclass as component

    @component
    class Position:
        x: float = 0.0
        y: float = 0.0

The dataclasses example in the README goes further and shows a Body component with a property that computes a local axis aligned bounding box, which is the idiomatic way to put derived data in a component: the derivation lives with the data, and the processing still lives in a processor.

Processors and the query that drives them

Processors, also commonly known as Systems, are where all processing logic is defined and executed. All Processors must inherit from the esper.Processor class and have a method called process, and other than that there are no restrictions: you can define any additional methods you might need.

The movement example is the canonical shape:

python
    class MovementProcessor(esper.Processor):

        def process(self):
            for ent, (vel, pos) in esper.get_components(Velocity, Position):
                pos.x += vel.x
                pos.y += vel.y

The single call that drives the whole thing is esper.process(), which the README describes as all that's needed for each iteration of your game loop.

The get_components() function is where the performance lives, and the README describes it as allowing efficient iteration over all Entities that contain the specified Component types. It can be used for querying two or more components at once, and tuple unpacking is necessary for the returned component pairs, so (vel, pos) rather than vel, pos.

Along with the components you get the entity id for the current pair, and the README gives the two common uses for it. If your processor needs to delete entities, you call esper.delete_entity() with that id. And if you want to add or remove a component on that entity as a result of some condition being met, you use the same id. The example given is an entity that should be deleted once its Lifecycle component reaches 0, handled in a LifecycleProcessor that takes a dt argument.

That last detail is worth noting because it shows the division of labour in practice. The processor holds the rule about when something dies, the component holds the remaining lifetime as data, and the entity id is the handle that ties them together. The library supplies no opinion about any of it.

Adding and removing things while the game runs

The general usage section of the README covers the mutating operations, and they matter because in a game loop the world never stops changing.

Entities, Components and Processors can be created, assigned or deleted while your game is running. That sounds obvious, and it is the thing a naive implementation gets wrong: if a processor is iterating over entities and one of them deletes another, the iteration has to cope.

The components side of it is the part with more moving parts. Adding and removing components on an existing entity is a separate operation from creating an entity with components, and the README treats both as first class. A component can be attached to an entity that already exists, which is how you would give a plain object a Behaviour once a condition is met, and removed again when it stops applying.

The reason this matters for performance is the same reason components are kept dumb. Because a component holds no references to other components, the library can maintain a per-type index of which entities carry which component, and a query for Velocity and Position is an intersection of two integer sets rather than a scan over every entity. Adding and removing a component at runtime is therefore an index update rather than a rebuild.

Boolean and conditional checks have their own section in the README's table of contents, as do more examples, and the repository's examples directory carries both a headless_example.py and the graphical ones, which is the practical way to see the whole API surface.

Event dispatching and the examples directory

Event dispatching is a separate feature from the ECS core, and it is the part of the README with the least explanation, so it is worth being careful about how much to claim.

The README lists Event Dispatching as its own top-level section, separate from the General Usage section, which tells you it is a distinct mechanism rather than a query variant. The README does not document its API in this section, so the details of how events are declared and delivered are not something to state without the API documentation on ReadTheDocs.

The examples directory is more informative about the intended use. There is a headless_example.py, which runs the library without any display, and a set of graphical examples built on different backends: pygame_example.py, pyglet_example.py, pysdl2_example.py, and pythonista_ios_example.py for running on iOS through Pythonista.

That list is a good summary of the library's ambitions. esper is written to run anywhere Python runs, including inside a mobile Pythonista environment, and the headless example is the one to look at if you are using it for a server side simulation rather than a game.

There are also two benchmark scripts, benchmark.py and benchmark_cache.py, and the repository carries a make.py at the root, along with a docs/ directory and a tests/ directory. The CI badge points at a unit-tests.yml workflow, so the test suite runs in CI.

Where esper fits, and where it stops

The comparison worth making is with the other Python ECS libraries, and the difference is a bet about dependencies and speed.

Most established Python ECS libraries ship a compiled extension, typically a Cython or C module, and they are faster as a result. esper takes the opposite position. The README states that it is written in 100% pure Python, so any compliant interpreter should work, and it is tested on both CPython and PyPy3. The claim in the project description is that the primary focus is to maximize performance while handling most common use cases, and the design is its own rather than a port, with the Entity System concepts being the inspiration rather than the code.

The bet is that for most game and simulation workloads, the overhead of a compiled extension is not where the time goes, and that a zero-dependency library that you can vendor into a project is worth more than the last increment of iteration speed. For a roguelike with a few thousand entities, that is a reasonable bet. For a project where the entity count is in the hundreds of thousands, the choice will be made for you.

The second limitation is structural. Entities are bare integer ids with no hierarchy, and there is no scene graph, no transform parent chain, no built-in spatial index. The Body component example in the README computes a local AABB as a property, and the word local is doing the work: the library gives you the data and the query, and the spatial structure is yours to build. The topics list names gamedev, rogue and rougelike, which is a good indication of the scale at which this was designed.

The third is that a pure Python ECS will not help you if your bottleneck is Python itself, and the project ships its own benchmarks rather than asking you to trust the marketing.

Editorial conclusion

esper suits a Python developer prototyping a roguelike or simulation who wants the ECS pattern without a native dependency or a compiled extension, since it is 100% pure Python and drops into a project as a single folder. It is the wrong tool if you have a performance budget that pure Python cannot meet, because the whole library trades native speed for zero dependencies and one-file installation, and it is the wrong tool if you want a scene graph or entity hierarchy, because the design deliberately keeps entities as bare integer ids with no parent-child relationship. Before adopting it, read the 3.0 migration notes, because version 3.0 removed the World object and moved its methods to module level functions, and run the headless_example.py in the examples directory to see the whole thing without a display.

Frequently asked questions

How do I install esper?

esper is a pure Python package with no dependencies. You can copy the esper folder straight into your project and import esper, install it from PyPI with pip install --user --upgrade esper, or install from the source directory with pip install . --user. The project requires Python 3.8 or later and is tested on both CPython and PyPy3.

What changed in esper 3.0?

Version 3.0 removes the World object and migrates its methods to module level functions, and multiple contexts can be created and switched between. The v2.x documentation is kept on the v2_maintenance branch if you need the older API.

How do I write a processor in esper?

A processor must inherit from the esper.Processor class and have a method called process, with no other restrictions. Inside it you iterate with esper.get_components(), which yields the entity id together with the component pair, and you call esper.process() once per iteration of your game loop.

What are the rules for components in esper?

Components should be simple Python classes or dataclasses and should not contain any processing logic. They may contain initialization logic and use properties to simplify data lookup, but game logic does not belong in them, and they should have no knowledge of other Components or Entities.

Does esper work without a display?

Yes. The examples directory includes a headless_example.py alongside the graphical pygame, pyglet, pysdl2 and pythonista_ios examples, so the library can be used for server side simulation as well as games.

Official sources

  1. benmoran56/esper on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/benmoran56-esper.svg)](https://hysenlabs.com/projects/benmoran56-esper)

Was this useful?