From c638dc1797ba5b8a63aa4e7d0334c09568fa19ec Mon Sep 17 00:00:00 2001 From: Rishab Motgi Date: Thu, 14 May 2026 13:13:02 -0700 Subject: [PATCH] fix: pre-bind handle variables to avoid Python 3.14 NameError (#1517) Python 3.14 changed asyncio.Task to eagerly inspect a coroutine's closure cells when task.set_name() is called (CPython issue gh-126004). If a closure cell is unbound at that moment the interpreter raises: NameError: cannot access free variable 'handle' where it is not associated with a value in enclosing scope Three places in _outbound_schedule_activity, _outbound_start_child_workflow, and _outbound_start_nexus_operation all declared 'handle' as a bare type annotation (no binding), created a coroutine that closes over it, and then passed that coroutine to the handle's constructor -- which triggered task naming before the outer assignment completed. Fix: initialise each handle variable to cast(, None) immediately before defining the coroutine. The cell is now bound (to None) so the eager inspection succeeds. The coroutine is only ever awaited after the outer handle = (...) line completes, so by the time any code inside run_activity / run_child / operation_handle_fn executes the variable holds the correct object. --- temporalio/worker/_workflow_instance.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/temporalio/worker/_workflow_instance.py b/temporalio/worker/_workflow_instance.py index fea97564b..9a442e841 100644 --- a/temporalio/worker/_workflow_instance.py +++ b/temporalio/worker/_workflow_instance.py @@ -1887,7 +1887,11 @@ def _outbound_schedule_activity( "Activity must have start_to_close_timeout or schedule_to_close_timeout" ) - handle: _ActivityHandle + # Pre-bind handle to None so the closure cell is not empty when + # Python 3.14's asyncio.Task inspects it eagerly during task naming. + # The coroutine only executes after the real _ActivityHandle is assigned + # below, so handle is always the correct object by the time it is used. + handle = cast(_ActivityHandle, None) # Function that runs in the handle async def run_activity() -> Any: @@ -1977,7 +1981,9 @@ async def _outbound_signal_external_workflow( async def _outbound_start_child_workflow( self, input: StartChildWorkflowInput ) -> _ChildWorkflowHandle: - handle: _ChildWorkflowHandle + # Pre-bind handle to None so the closure cell is not empty when + # Python 3.14's asyncio.Task inspects it eagerly during task naming. + handle = cast(_ChildWorkflowHandle, None) # Common code for handling cancel for start and run def apply_child_cancel_error() -> None: @@ -2042,7 +2048,9 @@ async def _outbound_start_nexus_operation( # resolved with an operation token). See comments in tests/worker/test_nexus.py for worked # examples of the evolution of the resulting handle state machine in the sync and async # Nexus response cases. - handle: _NexusOperationHandle[OutputT] + # Pre-bind handle to None so the closure cell is not empty when + # Python 3.14's asyncio.Task inspects it eagerly during task naming. + handle = cast(_NexusOperationHandle[OutputT], None) async def operation_handle_fn() -> OutputT: while True: