A Python-native event system with thread-safe, type-safe event handling and first-class async support.
PyESys brings clean, per-instance event handling to Python using familiar patterns like property descriptors and operator overloading. Perfect for real-time systems, simulations, and any application requiring robust event-driven architecture.
from pyesys import event
class Button:
@event
def on_click(self):
"""Click event signature"""
@on_click.emitter
def click(self):
print("Button clicked!")
# Each instance gets its own events
btn = Button()
btn.on_click += lambda: print("Handler executed!")
btn.click()
# Output: Button clicked!
# Handler executed!While Python has several event handling solutions, many common approaches present challenges for modern applications:
Event Bus Patterns: Many libraries use global event buses with string-based keys, which can create tight coupling and make per-instance event management complex. When each object instance needs its own events, you often need to manage ID strings or implement filtering logic.
Memory Management: Event systems with bound methods can suffer from memory leaks if not carefully designed with weak references and proper cleanup mechanisms.
Async/Sync Integration: Mixing synchronous and asynchronous event handlers consistently can be challenging, especially when you need both to coexist seamlessly.
PyESys addresses these architectural challenges with a clean, intuitive API:
- Per-Instance Events: No global registries or string-based keys. Each object manages its own events independently.
- Type Safety: Runtime signature validation catches handler mismatches early.
- Async-Ready: Mix sync and async handlers seamlessly with automatic thread pool handling.
- Pythonic: Familiar
@eventdecorator syntax inspired by@property, plus+=/-=operators. - Memory Safe: Built-in weak references prevent common memory leak patterns.
- Thread Safe: Safe concurrent event emission across multiple threads.
pip install pyesysRequires Python 3.12+. Zero dependencies.
from pyesys import create_event
# Create event with signature validation
event, listener = create_event(example=lambda msg: None)
def log_message(msg: str):
print(f"[LOG] {msg}")
# Subscribe and emit
listener += log_message
event.emit("Hello PyESys!")
# Output: [LOG] Hello PyESys!from pyesys import event
class FileProcessor:
@event
def on_progress(self, filename: str, percent: float):
"""Progress update event"""
@on_progress.emitter
def _update_progress(self, filename: str, percent: float):
pass # Event automatically emitted
def process(self, filename: str):
for i in range(0, 101, 25):
self._update_progress(filename, i)
# Each processor has independent events
processor = FileProcessor()
processor.on_progress += lambda f, p: print(f"{f}: {p}% complete")
processor.process("data.txt")
# Output: data.txt: 0% complete
# data.txt: 25% complete
# ...Create processing pipelines by chaining events between objects:
class DataProcessor:
@event
def on_processed(self, data: dict):
pass
@on_processed.emitter
def process(self, data: dict):
# Transform data
return {"processed": True, **data}
class DataValidator:
def validate(self, data: dict):
print(f"Validating: {data}")
processor = DataProcessor()
validator = DataValidator()
# Chain processors
processor.on_processed += validator.validate
processor.process({"id": 123})Mix synchronous and asynchronous handlers effortlessly:
import asyncio
async def async_handler(data):
await asyncio.sleep(0.1)
print(f"Async: {data}")
def sync_handler(data):
print(f"Sync: {data}")
listener += [sync_handler, async_handler]
await event.emit_async("mixed-handlers")
# Both handlers run concurrentlyEfficiently manage multiple handlers:
# Bulk subscribe
listener += [handler1, handler2, handler3]
# Bulk unsubscribe
listener -= {handler1, handler2}
# Introspection
print(f"Active handlers: {listener.handler_count()}")def error_handler(exception, handler_func):
logger.error(f"Handler {handler_func.__name__} failed: {exception}")
event, listener = create_event(
example=lambda x: None,
error_handler=error_handler
)
# Failing handlers won't crash the system- Real-time Systems: React to sensor inputs and control signals
- Simulation Frameworks: Decouple models from visualization/control
- Plugin Architectures: Extend applications safely with event hooks
- UI/Backend Integration: Bridge sync and async worlds seamlessly
- Testable Systems: Replace complex callbacks with observable events
Creates an event emitter and listener pair with signature validation:
from pyesys import create_event
event, listener = create_event(
example=lambda x, y: None, # Handler signature template
allow_duplicates=False, # Prevent duplicate subscriptions
error_handler=custom_handler # Handle exceptions gracefully
)Creates class-level or module-level events using familiar decorator syntax:
from pyesys import event
# Class-level event (per-instance)
class MyClass:
@event
def on_something(self, value: int):
"""Event signature: handlers must accept a single int."""
pass
@on_something.emitter
def do_something(self, value: int):
"""This method triggers the on_something event after running."""
print(f"Doing something with {value}")
# Module-level event (global)
@event
def on_global_event(message: str):
"""Global event signature: handlers must accept a single str."""
pass
@on_global_event.emitter
def trigger_global(message: str):
"""This function triggers the module-level event after running."""
print(f"Global: {message}")The Event class provides the core event management functionality:
Subscription Management:
event += handler- Subscribe directly to eventevent -= handler- Unsubscribe directly from eventlistener += handler- Subscribe via listener interfacelistener -= handler- Unsubscribe via listener interfaceevent += [h1, h2, h3]- Bulk subscribeevent.clear()- Remove all handlers
Event Emission:
event.emit(*args, **kwargs)- Synchronous emissionawait event.emit_async(*args, **kwargs)- Asynchronous emission
Introspection:
event.handler_count()- Number of active handlersevent.handlers- List of current handlersbool(event)- True if handlers existlen(event)- Same as handler_count()
Type Safety: Runtime signature validation through example functions ensures handlers match expected signatures, catching errors early.
Memory Management: Automatic weak reference handling for bound methods prevents memory leaks without requiring manual cleanup.
Thread Safety: All operations are thread-safe, allowing safe concurrent access from multiple threads.
Async Integration: Seamlessly mix sync and async handlers. Async handlers run concurrently, sync handlers run in thread pools during async emission.
Error Resilience: Custom error handlers prevent one failing handler from affecting others, crucial for production systems.
For more control, instantiate events directly:
from pyesys.event import Event
def example_sig(a: int, b: str) -> None:
pass
msg_event = Event(example=example_sig)
def on_message(a: int, b: str) -> None:
print(f"Got {a} and {b}")
# Subscribe directly to the event
msg_event += on_message
msg_event.emit(1, "hello")
# Or use the listener interface
msg_event.listener += on_messageCreate application-wide events at module level:
from pyesys import event
@event
def on_user_login(user_id: str, timestamp: float) -> None:
"""Fired when a user successfully logs in"""
@on_user_login.emitter
def login_user(user_id: str, timestamp: float) -> None:
"""Authenticate user and emit login event"""
print(f"User {user_id} authenticated at {timestamp}")
# Subscribe to login events
def update_last_seen(user_id: str, timestamp: float):
print(f"Updating last seen for {user_id}")
on_user_login += update_last_seenPyESys uses pytest and pytest-asyncio. To install dev dependencies and run the test suite:
pip install -e .[dev]
pytest -qTest files live under tests/:
tests/unit/test_handler.py– tests forEventHandlertests/unit/test_event.py– tests forEvent(sync & async)tests/unit/test_prop.py– tests for the@eventdecoratortests/integration/test_pyesys_end_to_end.py– end-to-end integration tests
All tests must pass before merging any changes.
See CONTRIBUTING.md for details on:
- Setting up a development environment
- Branching and workflow conventions
- Coding style & formatting (PEP 8, Black, type hints)
- Writing tests and running them
- Submitting pull requests
PyESys - Pythonic events for modern applications 🐍✨