Custom Event Classes¶
While the base Event class works for simple cases, you can create custom event classes to carry domain-specific data and behavior.
Why Custom Events?¶
Custom event classes provide:
Type safety: IDEs can autocomplete event attributes
Documentation: Event structure is explicit in code
Validation: Ensure required data is present
Behavior: Add methods for common event operations
Clarity: Domain concepts are clearly represented
Creating Custom Events¶
Subclass Event and add your own attributes and methods:
from whistle import Event, EventDispatcher
class UserRegisteredEvent(Event):
"""Custom event for user registration with domain data."""
def __init__(self, user_id, username, email):
"""Initialize event with user data."""
self.user_id = user_id
self.username = username
self.email = email
def get_display_name(self):
"""Custom method on the event."""
return f"{self.username} ({self.email})"
def main():
"""Demonstrate custom event classes."""
dispatcher = EventDispatcher()
The custom event class:
Inherits from
Eventto getstop_propagation()and other base functionalityAdds domain-specific attributes (
user_id,username,email)Provides custom methods (
get_display_name())
Using Custom Events¶
Create instances and dispatch them:
print(f"Sending welcome email to {event.email}")
print(f" User: {event.get_display_name()}")
print(f" ID: {event.user_id}")
def log_registration(event):
print(f"New user registered: {event.username}")
# Register listeners
dispatcher.add_listener("user.registered", send_welcome_email)
dispatcher.add_listener("user.registered", log_registration)
# Create and dispatch custom event
event = UserRegisteredEvent(user_id=12345, username="alice", email="alice@example.com")
dispatcher.dispatch("user.registered", event)
if __name__ == "__main__":
main()
Running this produces:
Sending welcome email to alice@example.com
User: alice (alice@example.com)
ID: 12345
New user registered: alice
Listeners can access all custom attributes and methods on the event object.
Custom Event Patterns¶
Rich Domain Events¶
Include all relevant domain data:
class OrderPlacedEvent(Event):
def __init__(self, order_id, customer_id, items, total):
self.order_id = order_id
self.customer_id = customer_id
self.items = items
self.total = total
def get_item_count(self):
return sum(item.quantity for item in self.items)
def requires_shipping(self):
return any(item.physical for item in self.items)
Validation Events¶
Carry validation state:
class DataValidationEvent(Event):
def __init__(self, data):
self.data = data
self.errors = []
self.warnings = []
def add_error(self, message):
self.errors.append(message)
self.stop_propagation() # Stop on error
def add_warning(self, message):
self.warnings.append(message)
def is_valid(self):
return len(self.errors) == 0
Workflow Events¶
Track workflow state:
class WorkflowEvent(Event):
def __init__(self, workflow_id):
self.workflow_id = workflow_id
self.completed_steps = []
self.current_step = None
def mark_step_complete(self, step_name):
self.completed_steps.append(step_name)
def has_completed(self, step_name):
return step_name in self.completed_steps
Event Inheritance¶
Create event hierarchies for related events:
class BaseUserEvent(Event):
"""Base class for all user-related events."""
def __init__(self, user_id):
self.user_id = user_id
class UserRegisteredEvent(BaseUserEvent):
def __init__(self, user_id, email):
super().__init__(user_id)
self.email = email
class UserDeletedEvent(BaseUserEvent):
def __init__(self, user_id, reason):
super().__init__(user_id)
self.reason = reason
This allows listeners to handle specific events or all events in a hierarchy.
Immutable Events¶
Consider making events immutable for safety:
class ImmutableOrderEvent(Event):
def __init__(self, order_id, total):
self._order_id = order_id
self._total = total
@property
def order_id(self):
return self._order_id
@property
def total(self):
return self._total
This prevents listeners from accidentally modifying event data.
Best Practices¶
Keep events focused: One event per domain concept
Include all relevant data: Listeners shouldn’t need to query for more info
Make events immutable when possible to prevent unintended modifications
Document event structure: Docstrings should explain all attributes
Use type hints: Help IDEs and type checkers understand your events
Provide helper methods: Add convenience methods for common operations
Example with Type Hints¶
from typing import List
from whistle import Event
class ProductUpdatedEvent(Event):
"""Event dispatched when a product is updated.
Attributes:
product_id: Unique product identifier
changes: List of field names that changed
old_values: Dictionary of old values for changed fields
new_values: Dictionary of new values for changed fields
"""
def __init__(
self,
product_id: int,
changes: List[str],
old_values: dict,
new_values: dict
):
self.product_id = product_id
self.changes = changes
self.old_values = old_values
self.new_values = new_values
def was_field_changed(self, field_name: str) -> bool:
"""Check if a specific field was changed."""
return field_name in self.changes
See also Design Patterns for examples using custom events.