{"record":{"id":"095aa3b0ac692547","repo":"python/cpython","slug":"self-r-is-bound-to-a-different-event-loop","errorCode":null,"errorMessage":"{self!r} is bound to a different event loop","messagePattern":"(.+?) is bound to a different event loop","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"Lib/asyncio/mixins.py","lineNumber":20,"sourceCode":"\nimport threading\nfrom . import events\n\n_global_lock = threading.Lock()\n\n\nclass _LoopBoundMixin:\n    _loop = None\n\n    def _get_loop(self):\n        loop = events._get_running_loop()\n\n        if self._loop is None:\n            with _global_lock:\n                if self._loop is None:\n                    self._loop = loop\n        if loop is not self._loop:\n            raise RuntimeError(f'{self!r} is bound to a different event loop')\n        return loop\n","sourceCodeStart":2,"sourceCodeEnd":22,"githubUrl":"https://github.com/python/cpython/blob/bc6749cc3b5ae4a5e88a6cc2d5b3bebbe354eae6/Lib/asyncio/mixins.py#L2-L22","documentation":"All loop-bound asyncio primitives (Lock, Semaphore, Condition, Event, Queue, Barrier) lazily bind to the first event loop that uses them via _LoopBoundMixin._get_loop(). If the running loop later differs from the bound one, this RuntimeError fires. Since Python 3.10 the binding happens lazily (fixing 'attached to a different loop' at creation), so the error surfaces at first cross-loop use instead.","triggerScenarios":"Creating an asyncio.Lock/Queue at module level and using it under two different asyncio.run() calls; sharing a primitive between the main loop and a loop in another thread; tests that create a new loop per test but share a module-level Queue.","commonSituations":"Pytest-asyncio with function-scoped loops against module-scoped fixtures holding locks/queues; Jupyter's auto-running loop vs a manual asyncio.run; applications that restart the event loop (watchdogs, hot reload); multi-threaded servers with one loop per thread sharing globals.","solutions":["Create loop-bound primitives inside the coroutine/loop that owns them, not at import time","Scope fixtures to the loop lifetime: create the Queue/Lock in an async fixture","For cross-thread sharing, use loop.call_soon_threadsafe / run_coroutine_threadsafe instead of sharing primitives","Restructure to a single long-lived loop (asyncio.run once for the whole app)"],"exampleFix":"# before\nlock = asyncio.Lock()  # module level\nasync def main():\n    async with lock: ...\nasyncio.run(main())\nasyncio.run(main())  # second run -> different loop -> RuntimeError\n\n# after\nasync def main():\n    lock = asyncio.Lock()  # created inside the loop that uses it\n    async with lock: ...\nasyncio.run(main())","handlingStrategy":"validation","validationCode":"import asyncio\nprim_loop = getattr(prim, '_loop', None)\nrunning = asyncio.events._get_running_loop()\nconflict = prim_loop is not None and running is not None and prim_loop is not running","typeGuard":"def primitive_bound_to_running_loop(prim) -> bool:\n    import asyncio\n    bound = getattr(prim, '_loop', None)\n    running = asyncio.events._get_running_loop()\n    return bound is None or running is bound","tryCatchPattern":"try:\n    async with lock:\n        ...\nexcept RuntimeError as e:\n    if 'bound to a different event loop' in str(e):\n        # recreate the primitive inside this loop and retry once","preventionTips":["Create locks/queues/events inside the loop that uses them","Scope test fixtures to loop lifetime, not module lifetime","Run one long-lived loop per process; use threadsafe APIs to cross into it"],"tags":["asyncio","event-loop","synchronization","lifetime"],"backgroundTag":null,"analyzedSha":"bc6749cc3b5ae4a5e88a6cc2d5b3bebbe354eae6","analyzedAt":"2026-08-14T22:01:13.976Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}