This is an automated email from the ASF dual-hosted git repository.

shahar1 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 92a4e3ebea7 Fix XComArg.map() failing when the upstream XCom is a dict 
(#75022)
92a4e3ebea7 is described below

commit 92a4e3ebea7b788f479388788b0cc2a49cbe900b
Author: Justin Pakzad <[email protected]>
AuthorDate: Sat Nov 7 02:03:00 2026 -0500

    Fix XComArg.map() failing when the upstream XCom is a dict (#75022)
---
 .../dynamic-task-mapping.rst                       |  1 +
 task-sdk/src/airflow/sdk/definitions/xcom_arg.py   |  5 ++++-
 .../tests/task_sdk/definitions/test_xcom_arg.py    | 22 ++++++++++++++++++++++
 3 files changed, 27 insertions(+), 1 deletion(-)

diff --git 
a/airflow-core/docs/authoring-and-scheduling/dynamic-task-mapping.rst 
b/airflow-core/docs/authoring-and-scheduling/dynamic-task-mapping.rst
index 972f7440011..02d7038a0b6 100644
--- a/airflow-core/docs/authoring-and-scheduling/dynamic-task-mapping.rst
+++ b/airflow-core/docs/authoring-and-scheduling/dynamic-task-mapping.rst
@@ -534,6 +534,7 @@ There are a couple of things to note:
 
 #. The callable argument of :func:`map()` (``create_copy_kwargs`` in the 
example) **must not** be a task, but a plain Python function. The 
transformation is as a part of the "pre-processing" of the downstream task 
(i.e. ``copy_files``), not a standalone task in the Dag.
 #. The callable always take exactly one positional argument. This function is 
called for each item in the iterable used for task-mapping, similar to how 
Python's built-in :func:`map()` works.
+#. If the upstream value is a dict, the callable receives each item as a 
``(key, value)`` tuple.
 #. Since the callable is executed as a part of the downstream task, you can 
use any existing techniques to write the task function. To mark a component as 
skipped, for example, you should raise ``AirflowSkipException``. Note that 
returning ``None`` **does not** work here.
 
 Combining upstream data (aka "zipping")
diff --git a/task-sdk/src/airflow/sdk/definitions/xcom_arg.py 
b/task-sdk/src/airflow/sdk/definitions/xcom_arg.py
index 36b393c165f..fe608eea454 100644
--- a/task-sdk/src/airflow/sdk/definitions/xcom_arg.py
+++ b/task-sdk/src/airflow/sdk/definitions/xcom_arg.py
@@ -400,7 +400,10 @@ class _MapResult(Sequence):
     callables: MapCallables
 
     def __getitem__(self, index: Any) -> Any:
-        value = self.value[index]
+        if isinstance(self.value, dict):
+            value = list(self.value.items())[index]
+        else:
+            value = self.value[index]
 
         for f in self.callables:
             value = f(value)
diff --git a/task-sdk/tests/task_sdk/definitions/test_xcom_arg.py 
b/task-sdk/tests/task_sdk/definitions/test_xcom_arg.py
index 53b1139dacb..40bd01158c3 100644
--- a/task-sdk/tests/task_sdk/definitions/test_xcom_arg.py
+++ b/task-sdk/tests/task_sdk/definitions/test_xcom_arg.py
@@ -295,6 +295,28 @@ def test_xcom_map_raise_to_skip(run_ti, 
mock_supervisor_comms):
     assert result == ["a", "b"]
 
 
+def test_xcom_map_dict(run_ti, mock_supervisor_comms):
+    result = set()
+    with DAG("test") as dag:
+
+        @dag.task
+        def push():
+            return {"a": 1, "b": 2}
+
+        @dag.task
+        def pull(value):
+            result.add(value)
+
+        pull.expand(value=push().map(lambda kv: f"{kv[0]}={kv[1]}"))
+
+    mock_supervisor_comms.send.return_value = 
XComResult(key=BaseXCom.XCOM_RETURN_KEY, value={"a": 1, "b": 2})
+
+    for map_index in range(2):
+        assert run_ti(dag, "pull", map_index) == TaskInstanceState.SUCCESS
+
+    assert result == {"a=1", "b=2"}
+
+
 def test_xcom_concat(run_ti, mock_supervisor_comms):
     from airflow.sdk.definitions.xcom_arg import _ConcatResult
 

Reply via email to