vfdev-5: @asford code looks good, thanks !
I thought we could provide some more useful examples of its usage in FAQ or somewhere. What do you think ?
asford: I've added an example to the "concepts" doc section, matching the existing documentation for `add_event_handler`. @vfdev-5, can you take a peek and let me know if you think this is suitable?
diff --git a/docs/source/concepts.rst b/docs/source/concepts.rst
index c0e7ebbc..589a645a 100644
--- a/docs/source/concepts.rst
+++ b/docs/source/concepts.rst
@@ -88,6 +88,33 @@ Attaching an event handler is simple using method :meth:`~ignite.engine.Engine.a
 
     trainer.add_event_handler(Events.COMPLETED, on_training_ended, mydata)
 
+Event handlers can be detached via :meth:`~ignite.engine.Engine.remove_event_handler` or via the :class:`~ignite.engine.RemovableEventHandler`
+reference returned by :meth:`~ignite.engine.Engine.add_event_handler`. This can be used to reuse a configured engine for multiple loops:
+
+.. code-block:: python
+
+    model = ...
+    train_loader, validation_loader, test_loader = ...
+
+    trainer = create_supervised_trainer(model, optimizer, loss)
+    evaluator = create_supervised_evaluator(model, metrics={'acc': Accuracy()})
+
+    def log_metrics(engine, title):
+        print("Epoch: {} - {} accuracy: {:.2f}"
+               .format(trainer.state.epoch, title, engine.state.metrics['acc']))
+
+    @trainer.on(Events.EPOCH_COMPLETED)
+    def evaluate(trainer):
+        with evaluator.add_event_handler(Events.COMPLETED, log_metrics, "train"):
+            evaluator.run(train_loader)
+
+        with evaluator.add_event_handler(Events.COMPLETED, log_metrics, "validation"):
+            evaluator.run(validation_loader)
+
+        with evaluator.add_event_handler(Events.COMPLETED, log_metrics, "test"):
+            evaluator.run(test_loader)
+
+    trainer.run(train_loader, max_epochs=100)
 
 .. Note ::
 
diff --git a/docs/source/engine.rst b/docs/source/engine.rst
index d0ab677e..070b92ae 100644
--- a/docs/source/engine.rst
+++ b/docs/source/engine.rst
@@ -14,3 +14,7 @@ ignite.engine
    :undoc-members:
 
 .. autoclass:: State
+
+.. autoclass:: RemovableEventHandler
+   :members:
+   :undoc-members:
diff --git a/ignite/engine/engine.py b/ignite/engine/engine.py
index 79f79fb0..19c279cb 100644
--- a/ignite/engine/engine.py
+++ b/ignite/engine/engine.py
@@ -4,6 +4,7 @@ import sys
 import time
 from collections import defaultdict
 from enum import Enum
+import weakref
 
 from ignite._utils import _to_hours_mins_secs
 
@@ -48,6 +49,58 @@ class State(object):
         return getattr(self, State.event_to_attr[event_name])
 
 
+class RemovableEventHandle(object):
+    """A weakref handle to remove a registered event.
+
+    A handle that may be used to remove a registered event handler via the
+    remove method, with-statement, or context manager protocol. Returned from
+    :meth:`~ignite.engine.Engine.add_event_handler`.
+
+
+    Args:
+        event_name: Registered event name.
+        handler: Registered event handler, stored as weakref.
+        engine: Target engine, stored as weakref.
+
+    Example usage:
+
+    .. code-block:: python
+
+        engine = Engine()
+
+        def print_epoch(engine):
+            print("Epoch: {}".format(engine.state.epoch))
+
+        with engine.add_event_handler(Events.EPOCH_COMPLETED, print_epoch):
+            # print_epoch handler registered for a single run
+            engine.run(data)
+
+        # print_epoch handler is now unregistered
+    """
+
+    def __init__(self, event_name, handler, engine):
+        self.event_name = event_name
+        self.handler = weakref.ref(handler)
+        self.engine = weakref.ref(engine)
+
+    def remove(self):
+        """Remove handler from engine."""
+        handler = self.handler()
+        engine = self.engine()
+
+        if handler is None or engine is None:
+            return
+
+        if engine.has_event_handler(handler, self.event_name):
+            engine.remove_event_handler(handler, self.event_name)
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, type, value, tb):
+        self.remove()
+
+
 class Engine(object):
     """Runs a given process_function over each batch of a dataset, emitting events as it goes.
 
@@ -164,6 +217,9 @@ class Engine(object):
               Note that other arguments can be passed to the handler in addition to the `*args` and  `**kwargs`
               passed here, for example during :attr:`~ignite.engine.Events.EXCEPTION_RAISED`.
 
+        Returns:
+            :class:`~ignite.engine.RemovableEventHandler`, which can be used to remove the handler.
+
         Example usage:
 
         .. code-block:: python
@@ -186,6 +242,8 @@ class Engine(object):
         self._event_handlers[event_name].append((handler, args, kwargs))
         self._logger.debug("added handler for event %s.", event_name)
 
+        return RemovableEventHandle(event_name, handler, self)
+
     def has_event_handler(self, handler, event_name=None):
         """Check if the specified event has the specified handler.
 

[Feature Request] "Removable" event handlers.
_I'm happy to open a PR for this immediately if the feature seems useful._


Ignite's event interface provides both add and remove operations however the "remove" operation requires a reference to the original event handler. This makes temporary or one-off event handlers somewhat tricky to implement.

Pytorch's existing hook interface provides a solution to this issue by returning a "RemovableHandle" handle from `register_hook`, holding a weakref to the target hook to support later removal.

Ignite's `add_event_handler` should implement a similar interface, returning a "RemovableEventHandle" rather than `None`. For example, in an attrs-style syntax:

```python
@attr.s(auto_attribs=True)
class RemovableEventHandle:
    """Removable weak reference to an added event handler."""


    event_name: str
    handler: weakref.ref = attr.ib(converter=_as_weakref)
    engine: weakref.ref = attr.ib(converter=_as_weakref)

    def remove(self):
        """Remove handler from engine."""
        handler = self.handler()
        engine: Engine = self.engine()

        if handler and engine and engine.has_event_handler(handler):
            engine.remove_event_handler(handler, self.event_name)

    def __enter__(self):  # noqa: D
        return self

    def __exit__(self, type, value, tb):  # noqa: D
        self.remove()
```

Would support features like:

```python
engine = Engine(process_function)

def print_epoch(engine):
    print("Epoch: {}".format(engine.state.epoch))

# Train with a temporary event handler.
with engine.add_event_handler(Events.EPOCH_COMPLETED, print_epoch):
    engine.run(dataloader, 10)
```
diff --git a/tests/ignite/engine/test_engine.py b/tests/ignite/engine/test_engine.py
index 624f1d5a..a591350e 100644
--- a/tests/ignite/engine/test_engine.py
+++ b/tests/ignite/engine/test_engine.py
@@ -1,5 +1,6 @@
 from __future__ import division
 from enum import Enum
+import gc
 
 import pytest
 from mock import call, MagicMock, Mock
@@ -130,6 +131,85 @@ def test_adding_multiple_event_handlers():
         handler.assert_called_once_with(engine)
 
 
+def test_event_removable_handle():
+
+    # Removable handle removes event from engine.
+    engine = DummyEngine()
+    handler = MagicMock()
+
+    removable_handle = engine.add_event_handler(Events.STARTED, handler)
+    assert engine.has_event_handler(handler, Events.STARTED)
+
+    engine.run(1)
+    handler.assert_called_once_with(engine)
+
+    removable_handle.remove()
+    assert not engine.has_event_handler(handler, Events.STARTED)
+
+    # Second engine pass does not fire handle again.
+    engine.run(1)
+    handler.assert_called_once_with(engine)
+
+    # Removable handle can be used as a context manager
+    handler = MagicMock()
+
+    with engine.add_event_handler(Events.STARTED, handler):
+        assert engine.has_event_handler(handler, Events.STARTED)
+        engine.run(1)
+
+    assert not engine.has_event_handler(handler, Events.STARTED)
+    handler.assert_called_once_with(engine)
+
+    engine.run(1)
+    handler.assert_called_once_with(engine)
+
+    # Removeable handle only effects a single event registration
+    handler = MagicMock()
+
+    with engine.add_event_handler(Events.STARTED, handler):
+        with engine.add_event_handler(Events.COMPLETED, handler):
+            assert engine.has_event_handler(handler, Events.STARTED)
+            assert engine.has_event_handler(handler, Events.COMPLETED)
+        assert engine.has_event_handler(handler, Events.STARTED)
+        assert not engine.has_event_handler(handler, Events.COMPLETED)
+    assert not engine.has_event_handler(handler, Events.STARTED)
+    assert not engine.has_event_handler(handler, Events.COMPLETED)
+
+    # Removeable handle is re-enter and re-exitable
+
+    handler = MagicMock()
+
+    remove = engine.add_event_handler(Events.STARTED, handler)
+
+    with remove:
+        with remove:
+            assert engine.has_event_handler(handler, Events.STARTED)
+        assert not engine.has_event_handler(handler, Events.STARTED)
+    assert not engine.has_event_handler(handler, Events.STARTED)
+
+    # Removeable handle is a weakref, does not keep engine or event alive
+    def _add_in_closure():
+        _engine = DummyEngine()
+
+        def _handler(_):
+            pass
+
+        _handle = _engine.add_event_handler(Events.STARTED, _handler)
+        assert _handle.engine() is _engine
+        assert _handle.handler() is _handler
+
+        return _handle
+
+    removable_handle = _add_in_closure()
+
+    # gc.collect, resolving reference cycles in engine/state
+    # required to ensure object deletion in python2
+    gc.collect()
+
+    assert removable_handle.engine() is None
+    assert removable_handle.handler() is None
+
+
 def test_has_event_handler():
     engine = DummyEngine()
     handlers = [MagicMock(), MagicMock()]

pip install -e .
parse_log_pytest
pytest
numpy mock pytest codecov pytest-cov matplotlib pandas gym tqdm scikit-learn tensorboardX visdom polyaxon-client mlflow
apt-get update apt-get install -y gcc
3.6
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alembic==1.7.7
attrs @ file:///opt/conda/conda-bld/attrs_1642510447205/work
certifi==2021.5.30
charset-normalizer==2.0.12
click==7.1.2
cloudpickle==2.2.1
codecov==2.1.13
coverage==6.2
cycler==0.11.0
databricks-cli==0.17.8
dataclasses==0.8
decorator==4.4.2
docker==5.0.3
entrypoints==0.4
Flask==1.1.4
gitdb==4.0.9
GitPython==3.1.18
greenlet==2.0.2
gunicorn==21.2.0
gym==0.26.2
gym-notices==0.0.8
hestia==0.6.0
idna==3.10
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1631916693255/work
importlib-resources==5.4.0
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
itsdangerous==1.1.0
Jinja2==2.10.3
joblib==1.1.1
jsonpatch==1.32
jsonpointer==2.3
kiwisolver==1.3.1
Mako==1.1.6
MarkupSafe==2.0.1
marshmallow==3.0.0rc5
matplotlib==3.3.4
mlflow==1.23.1
mock==5.2.0
more-itertools @ file:///tmp/build/80754af9/more-itertools_1637733554872/work
networkx==2.5.1
numpy==1.19.5
oauthlib==3.2.2
packaging @ file:///tmp/build/80754af9/packaging_1637314298585/work
pandas==1.1.5
Pillow==8.4.0
pluggy @ file:///tmp/build/80754af9/pluggy_1615976315926/work
polyaxon-client==0.6.1
polyaxon-schemas==0.6.1
polystores==0.2.5
prometheus-client==0.17.1
prometheus_flask_exporter==0.23.2
protobuf==4.21.0
psutil==7.0.0
py @ file:///opt/conda/conda-bld/py_1644396412707/work
PyJWT==2.4.0
pyparsing @ file:///tmp/build/80754af9/pyparsing_1635766073266/work
pytest==6.2.4
pytest-cov==4.0.0
python-dateutil==2.9.0.post0
-e git+https://github.com/pytorch/ignite.git@c5e6c70f0e661fbd8abff345fd3168ac6877f64d#egg=pytorch_ignite
pytz==2025.2
PyYAML==6.0.1
querystring-parser==1.2.4
requests==2.27.1
requests-toolbelt==1.0.0
rhea==0.5.5
scikit-learn==0.24.2
scipy==1.5.4
six==1.17.0
smmap==5.0.0
SQLAlchemy==1.4.54
sqlparse==0.4.4
tabulate==0.8.10
tensorboardX==2.6.2.2
threadpoolctl==3.1.0
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
tomli==1.2.3
torch==1.10.2
tornado==6.1
tqdm==4.64.1
typing_extensions @ file:///opt/conda/conda-bld/typing_extensions_1647553014482/work
urllib3==1.26.20
visdom==0.2.4
websocket-client==1.3.1
Werkzeug==1.0.1
zipp @ file:///tmp/build/80754af9/zipp_1633618647012/work

name: ignite
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=21.4.0=pyhd3eb1b0_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - importlib-metadata=4.8.1=py36h06a4308_0
  - importlib_metadata=4.8.1=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - more-itertools=8.12.0=pyhd3eb1b0_0
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=21.3=pyhd3eb1b0_0
  - pip=21.2.2=py36h06a4308_0
  - pluggy=0.13.1=py36h06a4308_0
  - py=1.11.0=pyhd3eb1b0_0
  - pyparsing=3.0.4=pyhd3eb1b0_0
  - pytest=6.2.4=py36h06a4308_2
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd3eb1b0_0
  - typing_extensions=4.1.1=pyh06a4308_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.6.0=pyhd3eb1b0_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alembic==1.7.7
      - charset-normalizer==2.0.12
      - click==7.1.2
      - cloudpickle==2.2.1
      - codecov==2.1.13
      - coverage==6.2
      - cycler==0.11.0
      - databricks-cli==0.17.8
      - dataclasses==0.8
      - decorator==4.4.2
      - docker==5.0.3
      - entrypoints==0.4
      - flask==1.1.4
      - gitdb==4.0.9
      - gitpython==3.1.18
      - greenlet==2.0.2
      - gunicorn==21.2.0
      - gym==0.26.2
      - gym-notices==0.0.8
      - hestia==0.6.0
      - idna==3.10
      - importlib-resources==5.4.0
      - itsdangerous==1.1.0
      - jinja2==2.10.3
      - joblib==1.1.1
      - jsonpatch==1.32
      - jsonpointer==2.3
      - kiwisolver==1.3.1
      - mako==1.1.6
      - markupsafe==2.0.1
      - marshmallow==3.0.0rc5
      - matplotlib==3.3.4
      - mlflow==1.23.1
      - mock==5.2.0
      - networkx==2.5.1
      - numpy==1.19.5
      - oauthlib==3.2.2
      - pandas==1.1.5
      - pillow==8.4.0
      - polyaxon-client==0.6.1
      - polyaxon-schemas==0.6.1
      - polystores==0.2.5
      - prometheus-client==0.17.1
      - prometheus-flask-exporter==0.23.2
      - protobuf==4.21.0
      - psutil==7.0.0
      - pyjwt==2.4.0
      - pytest-cov==4.0.0
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - pyyaml==6.0.1
      - querystring-parser==1.2.4
      - requests==2.27.1
      - requests-toolbelt==1.0.0
      - rhea==0.5.5
      - scikit-learn==0.24.2
      - scipy==1.5.4
      - six==1.17.0
      - smmap==5.0.0
      - sqlalchemy==1.4.54
      - sqlparse==0.4.4
      - tabulate==0.8.10
      - tensorboardx==2.6.2.2
      - threadpoolctl==3.1.0
      - tomli==1.2.3
      - torch==1.10.2
      - tornado==6.1
      - tqdm==4.64.1
      - urllib3==1.26.20
      - visdom==0.2.4
      - websocket-client==1.3.1
      - werkzeug==1.0.1
prefix: /opt/conda/envs/ignite

tests/ignite/engine/test_engine.py::test_event_removable_handle
tests/ignite/engine/test_engine.py::test_terminate tests/ignite/engine/test_engine.py::test_invalid_process_raises_with_invalid_signature tests/ignite/engine/test_engine.py::test_add_event_handler_raises_with_invalid_event tests/ignite/engine/test_engine.py::test_add_event_handler_raises_with_invalid_signature tests/ignite/engine/test_engine.py::test_add_event_handler tests/ignite/engine/test_engine.py::test_adding_multiple_event_handlers tests/ignite/engine/test_engine.py::test_has_event_handler tests/ignite/engine/test_engine.py::test_remove_event_handler tests/ignite/engine/test_engine.py::test_args_and_kwargs_are_passed_to_event tests/ignite/engine/test_engine.py::test_custom_events tests/ignite/engine/test_engine.py::test_custom_events_with_event_to_attr tests/ignite/engine/test_engine.py::test_on_decorator_raises_with_invalid_event tests/ignite/engine/test_engine.py::test_on_decorator tests/ignite/engine/test_engine.py::test_returns_state tests/ignite/engine/test_engine.py::test_state_attributes tests/ignite/engine/test_engine.py::test_default_exception_handler tests/ignite/engine/test_engine.py::test_custom_exception_handler tests/ignite/engine/test_engine.py::test_current_epoch_counter_increases_every_epoch tests/ignite/engine/test_engine.py::test_current_iteration_counter_increases_every_iteration tests/ignite/engine/test_engine.py::test_stopping_criterion_is_max_epochs tests/ignite/engine/test_engine.py::test_terminate_at_end_of_epoch_stops_run tests/ignite/engine/test_engine.py::test_terminate_at_start_of_epoch_stops_run_after_completing_iteration tests/ignite/engine/test_engine.py::test_terminate_stops_run_mid_epoch tests/ignite/engine/test_engine.py::test_terminate_epoch_stops_mid_epoch tests/ignite/engine/test_engine.py::test_iteration_events_are_fired tests/ignite/engine/test_engine.py::test_create_supervised_trainer tests/ignite/engine/test_engine.py::test_create_supervised_trainer_with_cpu tests/ignite/engine/test_engine.py::test_create_supervised_trainer_traced_with_cpu tests/ignite/engine/test_engine.py::test_create_supervised tests/ignite/engine/test_engine.py::test_create_supervised_on_cpu tests/ignite/engine/test_engine.py::test_create_supervised_evaluator_traced_on_cpu tests/ignite/engine/test_engine.py::test_create_supervised_with_metrics tests/ignite/engine/test_engine.py::test_reset_should_terminate
ignite/engine/engine.py docs/source/engine.rst docs/source/concepts.rst
ignite/engine/engine.py docs/source/engine.rst docs/source/concepts.rst