Repository: deepset-ai/haystack

Issue:
Improve type validation to handle bare typing objects
**Is your feature request related to a problem? Please describe.**
Our current type validation used when validating pipeline connections doesn't support comparison of bare types. For example, `List[Any]` does not get matched to `List` even though it should.

**Describe the solution you'd like**
Update the function such that we can handle bare types. For example, this code 
```python
    # If either is a bare type (no args), treat it as if it had Any
    if not sender_args:
        sender_args = (Any,)
    if not receiver_args:
        receiver_args = (Any,) * len(sender_args)
```
could be used such that any bare types are given `Any` as arguments. 

Alternatively we can look for existing type comparison solutions that probably exist in other libraries that handles this already.

**Describe alternatives you've considered**
Leave as is and probably document that we don't properly support bare types.



Discussion:
Here are some tests (not exhaustive) we can use while developing this

```python
@pytest.mark.parametrize(
    "sender_type,receiver_type",
    [
        pytest.param(List[int], List, id="list-of-primitive-to-bare-list"),
        pytest.param(Dict[str, int], Dict, id="dict-of-primitive-to-bare-dict"),
        pytest.param(Set[float], Set, id="set-of-primitive-to-bare-set"),
        pytest.param(Tuple[int, str], Tuple, id="tuple-of-primitive-to-bare-tuple"),
        pytest.param(Callable[[int, str], bool], Callable, id="callable-to-bare-callable"),
    ],
)
def test_container_of_primitive_to_bare_container_strict(sender_type, receiver_type):
    assert _types_are_compatible(sender_type, receiver_type)
    # Bare container types should not be compatible with their typed counterparts
    assert not _types_are_compatible(receiver_type, sender_type)


@pytest.mark.parametrize(
    "sender_type,receiver_type",
    [
        pytest.param(List[Any], List, id="list-of-any-to-bare-list"),
        pytest.param(Dict[Any, Any], Dict, id="dict-of-any-to-bare-dict"),
        pytest.param(Set[Any], Set, id="set-of-any-to-bare-set"),
        pytest.param(Tuple[Any], Tuple, id="tuple-of-any-to-bare-tuple"),
    ],
)
def test_container_of_any_to_bare_container_strict(sender_type, receiver_type):
    # Both are compatible
    assert _types_are_compatible(sender_type, receiver_type)
    assert _types_are_compatible(receiver_type, sender_type)


@pytest.mark.parametrize(
    "sender_type,receiver_type",
    [
        pytest.param(Literal["test"], Literal, id="literal-to-bare-literal"),
        pytest.param(Union[str, int], Union, id="union-to-bare-union"),
        pytest.param(Optional[str], Optional, id="union-to-bare-union"),
    ],
)
def test_always_incompatible_bare_types(sender_type, receiver_type):
    # Neither are compatible
    assert not _types_are_compatible(sender_type, receiver_type)
    assert not _types_are_compatible(receiver_type, sender_type)
```



Patch:
diff --git a/haystack/core/type_utils.py b/haystack/core/type_utils.py
index 4b84c74dc0..162fcc759f 100644
--- a/haystack/core/type_utils.py
+++ b/haystack/core/type_utils.py
@@ -64,6 +64,12 @@ def _strict_types_are_compatible(sender, receiver):  # pylint: disable=too-many-
     # Compare generic type arguments
     sender_args = get_args(sender)
     receiver_args = get_args(receiver)
+
+    # Handle bare types
+    if not sender_args and sender_origin:
+        sender_args = (Any,)
+    if not receiver_args and receiver_origin:
+        receiver_args = (Any,) * (len(sender_args) if sender_args else 1)
     if len(sender_args) > len(receiver_args):
         return False
 
diff --git a/releasenotes/notes/add-bare-type-validation-a4720bc66ccaf5ef.yaml b/releasenotes/notes/add-bare-type-validation-a4720bc66ccaf5ef.yaml
new file mode 100644
index 0000000000..0fa8f21a4f
--- /dev/null
+++ b/releasenotes/notes/add-bare-type-validation-a4720bc66ccaf5ef.yaml
@@ -0,0 +1,4 @@
+---
+features:
+  - |
+    Treat bare types (e.g., List, Dict) as generic types with Any arguments during type compatibility checks.


Test patch:
diff --git a/test/core/pipeline/test_type_utils.py b/test/core/pipeline/test_type_utils.py
index 89e0f02122..9151b2770c 100644
--- a/test/core/pipeline/test_type_utils.py
+++ b/test/core/pipeline/test_type_utils.py
@@ -354,13 +354,87 @@ def test_partially_overlapping_unions_are_not_compatible_strict(sender_type, rec
     assert not _types_are_compatible(sender_type, receiver_type)
 
 
+@pytest.mark.parametrize(
+    "sender_type,receiver_type", [pytest.param((List[int]), list, id="list-of-primitive-to-list-object")]
+)
+def test_list_of_primitive_to_list(sender_type, receiver_type):
+    """This currently doesn't work because we don't handle primitive types."""
+    assert not _types_are_compatible(sender_type, receiver_type)
+
+
 @pytest.mark.parametrize(
     "sender_type,receiver_type",
     [
-        pytest.param((List[int]), List, id="list-of-primitive-to-bare-list"),
-        pytest.param((List[int]), list, id="list-of-primitive-to-list-object"),
+        pytest.param(List[int], List, id="list-of-primitive-to-bare-list"),
+        pytest.param(Dict[str, int], Dict, id="dict-of-primitive-to-bare-dict"),
+        pytest.param(Set[float], Set, id="set-of-primitive-to-bare-set"),
+        pytest.param(Tuple[int, str], Tuple, id="tuple-of-primitive-to-bare-tuple"),
     ],
 )
-def test_list_of_primitive_to_list(sender_type, receiver_type):
-    """This currently doesn't work because we don't handle bare types without arguments."""
+def test_container_of_primitive_to_bare_container_strict(sender_type, receiver_type):
+    assert _types_are_compatible(sender_type, receiver_type)
+    # Bare container types should not be compatible with their typed counterparts
+    assert not _types_are_compatible(receiver_type, sender_type)
+
+
+@pytest.mark.parametrize(
+    "sender_type,receiver_type",
+    [
+        pytest.param(List[Any], List, id="list-of-any-to-bare-list"),
+        pytest.param(Dict[Any, Any], Dict, id="dict-of-any-to-bare-dict"),
+        pytest.param(Set[Any], Set, id="set-of-any-to-bare-set"),
+        pytest.param(Tuple[Any], Tuple, id="tuple-of-any-to-bare-tuple"),
+    ],
+)
+def test_container_of_any_to_bare_container_strict(sender_type, receiver_type):
+    # Both are compatible
+    assert _types_are_compatible(sender_type, receiver_type)
+    assert _types_are_compatible(receiver_type, sender_type)
+
+
+@pytest.mark.parametrize(
+    "sender_type,receiver_type",
+    [
+        pytest.param(Literal["test"], Literal, id="literal-to-bare-literal"),
+        pytest.param(Union[str, int], Union, id="union-to-bare-union"),
+        pytest.param(Optional[str], Optional, id="optional-to-bare-optional"),
+    ],
+)
+def test_always_incompatible_bare_types(sender_type, receiver_type):
+    # Neither are compatible
     assert not _types_are_compatible(sender_type, receiver_type)
+    assert not _types_are_compatible(receiver_type, sender_type)
+
+
+@pytest.mark.parametrize(
+    "sender_type,receiver_type",
+    [
+        # Nested Lists
+        pytest.param(List[List[int]], List[List], id="nested-list-to-partially-bare-list"),
+        pytest.param(List[List[int]], List, id="nested-list-to-bare-list"),
+        # Nested Dicts
+        pytest.param(Dict[str, Dict[str, int]], Dict[str, Dict], id="nested-dict-to-partially-bare-dict"),
+        pytest.param(Dict[str, Dict[str, int]], Dict, id="nested-dict-to-bare-dict"),
+        # Nested Sets
+        pytest.param(Set[Set[int]], Set[Set], id="nested-set-to-partially-bare-set"),
+        pytest.param(Set[Set[int]], Set, id="nested-set-to-bare-set"),
+        # Nested Tuples
+        pytest.param(Tuple[Tuple[int, str], bool], Tuple[Tuple, bool], id="nested-tuple-to-partially-bare-tuple"),
+        pytest.param(Tuple[Tuple[int, str], bool], Tuple, id="nested-tuple-to-bare-tuple"),
+        # Mixed nesting
+        pytest.param(List[Dict[str, int]], List[Dict], id="list-of-dict-to-list-of-bare-dict"),
+        pytest.param(Dict[str, List[int]], Dict[str, List], id="dict-with-list-to-dict-with-bare-list"),
+        pytest.param(Set[Tuple[int, str]], Set[Tuple], id="set-of-tuple-to-set-of-bare-tuple"),
+        pytest.param(
+            Tuple[List[int], Dict[str, bool]], Tuple[List, Dict], id="tuple-of-containers-to-tuple-of-bare-containers"
+        ),
+        # Triple nesting
+        pytest.param(List[List[List[int]]], List[List[List]], id="triple-nested-list-to-partially-bare"),
+        pytest.param(List[List[List[int]]], List[List], id="triple-nested-list-to-double-bare"),
+        pytest.param(List[List[List[int]]], List, id="triple-nested-list-to-completely-bare"),
+    ],
+)
+def test_nested_container_compatibility(sender_type, receiver_type):
+    assert _types_are_compatible(sender_type, receiver_type)
+    # Bare container types should not be compatible with their typed counterparts
+    assert not _types_are_compatible(receiver_type, sender_type)