diff --git a/airflow/decorators/base.py b/airflow/decorators/base.py index 7da74e5514e42..89f06b6aab591 100644 --- a/airflow/decorators/base.py +++ b/airflow/decorators/base.py @@ -300,6 +300,10 @@ def _infer_multiple_outputs(self): except TypeError: # Can't evaluate return type. return False ttype = getattr(return_type, "__origin__", return_type) + if ttype == dict or ttype == Dict: + raise AttributeError( + "multiple_outputs was not set and will not implicitly unroll dict for return values" + ) return ttype == dict or ttype == Dict def __attrs_post_init__(self): diff --git a/docs/apache-airflow/tutorial/taskflow.rst b/docs/apache-airflow/tutorial/taskflow.rst index 9db581200effe..497509102c5d7 100644 --- a/docs/apache-airflow/tutorial/taskflow.rst +++ b/docs/apache-airflow/tutorial/taskflow.rst @@ -377,19 +377,21 @@ section "Having sensors return XOM values" of :doc:`apache-airflow-providers:how Multiple outputs inference -------------------------- -Tasks can also infer multiple outputs by using dict Python typing. +In case a task has multiple return values, Airflow can unroll these in separate values if using +Python dict typing. The values then become available as separate arguments to downstream tasks. .. code-block:: python - @task + @task(multiple_outputs=True) def identity_dict(x: int, y: int) -> dict[str, int]: return {"x": x, "y": y} By using the typing ``Dict`` for the function return type, the ``multiple_outputs`` parameter is automatically set to true. -Note, If you manually set the ``multiple_outputs`` parameter the inference is disabled and -the parameter value is used. +Note, before Airflow 2.5, Airflow would automatically unroll ``dict`` return values and you would need to set +``multiple_outputs=False` explicitly. This was deemed confusing and now always defaults to ``False``. + Adding dependencies between decorated and traditional tasks ----------------------------------------------------------- diff --git a/newsfragments/27819.significant.rst b/newsfragments/27819.significant.rst new file mode 100644 index 0000000000000..ad0fc169467f0 --- /dev/null +++ b/newsfragments/27819.significant.rst @@ -0,0 +1,2 @@ +Airflow no longer automatically unrolls dict values into separate arguments requiring an explicit +``multiple_outputs=True`` if unrolling is required. diff --git a/tests/decorators/test_python.py b/tests/decorators/test_python.py index 3ded6c80fa337..0e95494b3b517 100644 --- a/tests/decorators/test_python.py +++ b/tests/decorators/test_python.py @@ -94,19 +94,14 @@ def test_python_operator_python_callable_is_callable(self): "Dict[str, int]", ], ) - def test_infer_multiple_outputs_using_dict_typing(self, resolve, annotation): - @task_decorator - def identity_dict(x: int, y: int) -> resolve(annotation): - return {"x": x, "y": y} - - assert identity_dict(5, 5).operator.multiple_outputs is True + def test_infer_multiple_outputs_using_dict_typing_raises(self, resolve, annotation): + with pytest.raises(AttributeError) as e: - # Check invoking ``@task_decorator.__call__()`` yields the correct inference. - @task_decorator() - def identity_dict_with_decorator_call(x: int, y: int) -> resolve(annotation): - return {"x": x, "y": y} + @task_decorator() + def identity_dict(x: int, y: int) -> resolve(annotation): + return {"x": x, "y": y} - assert identity_dict_with_decorator_call(5, 5).operator.multiple_outputs is True + assert "not implicitly unroll" in str(e.value) def test_infer_multiple_outputs_using_other_typing(self): @task_decorator