Hook into mapper, instance, and session lifecycle events to extend ORM behavior
SQLAlchemy’s event system provides hooks into critical points in the ORM lifecycle, allowing you to execute custom logic during object creation, loading, persistence, and more.
Called after an instance is loaded from the database:
@event.listens_for(User, "load")def receive_load(target, context): print(f"User {target.name} loaded from database")
Use cases:
Initialize transient attributes not stored in the database
Set up computed properties
Perform post-load validation
The load event fires before eager loading completes. Accessing unloaded relationships may trigger additional queries or interfere with the loading process. Use restore_load_context=True if you need to access attributes that weren’t part of the initial load.
Called when an instance is constructed via __init__():
@event.listens_for(User, "init")def receive_init(target, args, kwargs): print(f"Creating new user: {kwargs.get('name')}") # You can modify kwargs before __init__ is called kwargs.setdefault('created_at', datetime.now())
Key points:
Only fires for userland construction (not database loads)
Called after attributes are refreshed from a query:
@event.listens_for(User, "refresh")def receive_refresh(target, context, attrs): if attrs and 'name' in attrs: print(f"User name refreshed: {target.name}")
@event.listens_for(User, "before_delete")def receive_before_delete(mapper, connection, target): """Called before DELETE statement is emitted.""" # Archive the user before deletion connection.execute( text("INSERT INTO deleted_users SELECT * FROM users WHERE id = :id"), {"id": target.id} )@event.listens_for(User, "after_delete")def receive_after_delete(mapper, connection, target): """Called after DELETE statement completes.""" print(f"User {target.id} deleted")
@event.listens_for(User, "instrument_class")def receive_instrument_class(mapper, class_): """Called when mapper is first constructed.""" print(f"Instrumenting class: {class_.__name__}")@event.listens_for(User, "after_mapper_constructed")def receive_after_mapper_constructed(mapper, class_): """Called after mapper construction completes.""" # Access mapper properties print(f"Mapper has {len(list(mapper.iterate_properties))} properties")@event.listens_for(User, "mapper_configured")def receive_mapper_configured(mapper, class_): """Called after mapper is fully configured.""" print(f"Mapper configured for {class_.__name__}")
Understanding mapper configuration timing
The mapper configuration process has several phases:
instrument_class: Earliest phase, minimal state available
after_mapper_constructed: Mapper properties are available
mapper_configured: Full configuration complete (except backrefs)
after_configured: All mappers configured (global event)
Choose the appropriate event based on what mapper state you need to access.
@event.listens_for(Session, "before_flush")def receive_before_flush(session, flush_context, instances): """Called before the flush process begins.""" # Safe to modify objects and add new objects for obj in session.new: if isinstance(obj, User): obj.updated_at = datetime.now()@event.listens_for(Session, "after_flush")def receive_after_flush(session, flush_context): """Called after flush completes, before commit.""" print(f"Flushed {len(session.identity_map)} objects")
before_flush is the recommended event for most flush-time operations because:
@event.listens_for(Session, "before_commit")def receive_before_commit(session): """Called before transaction commit.""" print("About to commit transaction")@event.listens_for(Session, "after_commit")def receive_after_commit(session): """Called after successful commit.""" # Safe to emit side effects like sending emails for obj in session.identity_map.values(): if isinstance(obj, User) and obj.should_notify: send_notification(obj)
@event.listens_for(Session, "transient_to_pending")def receive_transient_to_pending(session, instance): """Called when a new object is added to the session.""" print(f"Added new {instance.__class__.__name__}")
@event.listens_for(Session, "pending_to_persistent")def receive_pending_to_persistent(session, instance): """Called when a new object gets a database identity.""" print(f"New {instance.__class__.__name__} persisted with id {instance.id}")@event.listens_for(Session, "deleted_to_detached")def receive_deleted_to_detached(session, instance): """Called after a deleted object is removed from the session.""" print(f"{instance.__class__.__name__} deleted and detached")
@event.listens_for(Session, "before_flush")def before_flush(session, flush_context, instances): """Log all changes to an audit table.""" for obj in session.dirty: session.add(AuditLog( table_name=obj.__tablename__, record_id=obj.id, action="update", timestamp=datetime.now() )) for obj in session.new: session.add(AuditLog( table_name=obj.__tablename__, action="insert", timestamp=datetime.now() )) for obj in session.deleted: session.add(AuditLog( table_name=obj.__tablename__, record_id=obj.id, action="delete", timestamp=datetime.now() ))
@event.listens_for(User, "after_delete")def receive_after_delete(mapper, connection, target): """Clean up related data after user deletion.""" connection.execute( text("DELETE FROM user_preferences WHERE user_id = :id"), {"id": target.id} ) connection.execute( text("DELETE FROM user_sessions WHERE user_id = :id"), {"id": target.id} )