Repository: scikit-learn/scikit-learn
Pull request #31134: MNT Clean-up deprecations for 1.7: old tags
Author: login: jeremiedbb
Labels: name: module:utils
name: No Changelog Needed
Created: 2025-04-02T15:13:21Z | Merged: 2025-04-17T11:50:35Z

Removed deprecated old tags infrastructure.


Diff:
diff --git a/sklearn/base.py b/sklearn/base.py
index bff0bf18bed37..94aa51828aae5 100644
--- a/sklearn/base.py
+++ b/sklearn/base.py
@@ -30,14 +30,11 @@
 )
 from .utils.fixes import _IS_32BIT
 from .utils.validation import (
-    _check_feature_names,
     _check_feature_names_in,
-    _check_n_features,
     _generate_get_feature_names_out,
     _is_fitted,
     check_array,
     check_is_fitted,
-    validate_data,
 )
 
 
@@ -389,33 +386,6 @@ def __setstate__(self, state):
         except AttributeError:
             self.__dict__.update(state)
 
-    # TODO(1.7): Remove this method
-    def _more_tags(self):
-        """This code should never be reached since our `get_tags` will fallback on
-        `__sklearn_tags__` implemented below. We keep it for backward compatibility.
-        It is tested in `test_base_estimator_more_tags` in
-        `sklearn/utils/testing/test_tags.py`."""
-        from sklearn.utils._tags import _to_old_tags, default_tags
-
-        warnings.warn(
-            "The `_more_tags` method is deprecated in 1.6 and will be removed in "
-            "1.7. Please implement the `__sklearn_tags__` method.",
-            category=DeprecationWarning,
-        )
-        return _to_old_tags(default_tags(self))
-
-    # TODO(1.7): Remove this method
-    def _get_tags(self):
-        from sklearn.utils._tags import _to_old_tags, get_tags
-
-        warnings.warn(
-            "The `_get_tags` method is deprecated in 1.6 and will be removed in "
-            "1.7. Please implement the `__sklearn_tags__` method.",
-            category=DeprecationWarning,
-        )
-
-        return _to_old_tags(get_tags(self))
-
     def __sklearn_tags__(self):
         return Tags(
             estimator_type=None,
@@ -469,35 +439,6 @@ def _repr_mimebundle_(self, **kwargs):
             output["text/html"] = estimator_html_repr(self)
         return output
 
-    # TODO(1.7): Remove this method
-    def _validate_data(self, *args, **kwargs):
-        warnings.warn(
-            "`BaseEstimator._validate_data` is deprecated in 1.6 and will be removed "
-            "in 1.7. Use `sklearn.utils.validation.validate_data` instead. This "
-            "function becomes public and is part of the scikit-learn developer API.",
-            FutureWarning,
-        )
-        return validate_data(self, *args, **kwargs)
-
-    # TODO(1.7): Remove this method
-    def _check_n_features(self, *args, **kwargs):
-        warnings.warn(
-            "`BaseEstimator._check_n_features` is deprecated in 1.6 and will be "
-            "removed in 1.7. Use `sklearn.utils.validation._check_n_features` instead.",
-            FutureWarning,
-        )
-        _check_n_features(self, *args, **kwargs)
-
-    # TODO(1.7): Remove this method
-    def _check_feature_names(self, *args, **kwargs):
-        warnings.warn(
-            "`BaseEstimator._check_feature_names` is deprecated in 1.6 and will be "
-            "removed in 1.7. Use `sklearn.utils.validation._check_feature_names` "
-            "instead.",
-            FutureWarning,
-        )
-        _check_feature_names(self, *args, **kwargs)
-
 
 class ClassifierMixin:
     """Mixin class for all classifiers in scikit-learn.
diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py
index 13b9599ffc5e0..122b9508da86a 100644
--- a/sklearn/pipeline.py
+++ b/sklearn/pipeline.py
@@ -1221,7 +1221,7 @@ def __sklearn_tags__(self):
             tags.input_tags.sparse = all(
                 get_tags(step).input_tags.sparse
                 for name, step in self.steps
-                if step != "passthrough"
+                if step is not None and step != "passthrough"
             )
         except (ValueError, AttributeError, TypeError):
             # This happens when the `steps` is not a list of (name, estimator)
diff --git a/sklearn/tests/test_common.py b/sklearn/tests/test_common.py
index f916f7e9862a5..227e2d7663500 100644
--- a/sklearn/tests/test_common.py
+++ b/sklearn/tests/test_common.py
@@ -19,7 +19,6 @@
 import sklearn
 from sklearn.base import BaseEstimator
 from sklearn.compose import ColumnTransformer
-from sklearn.datasets import make_classification
 from sklearn.exceptions import ConvergenceWarning
 
 # make it possible to discover experimental estimators when calling `all_estimators`
@@ -401,37 +400,3 @@ def test_check_inplace_ensure_writeable(estimator):
         estimator.set_params(kernel="precomputed")
 
     check_inplace_ensure_writeable(name, estimator)
-
-
-# TODO(1.7): Remove this test when the deprecation cycle is over
-def test_transition_public_api_deprecations():
-    """This test checks that we raised deprecation warning explaining how to transition
-    to the new developer public API from 1.5 to 1.6.
-    """
-
-    class OldEstimator(BaseEstimator):
-        def fit(self, X, y=None):
-            X = self._validate_data(X)
-            self._check_n_features(X, reset=True)
-            self._check_feature_names(X, reset=True)
-            return self
-
-        def transform(self, X):
-            return X  # pragma: no cover
-
-    X, y = make_classification(n_samples=10, n_features=5, random_state=0)
-
-    old_estimator = OldEstimator()
-    with pytest.warns(FutureWarning) as warning_list:
-        old_estimator.fit(X)
-
-    assert len(warning_list) == 3
-    assert str(warning_list[0].message).startswith(
-        "`BaseEstimator._validate_data` is deprecated"
-    )
-    assert str(warning_list[1].message).startswith(
-        "`BaseEstimator._check_n_features` is deprecated"
-    )
-    assert str(warning_list[2].message).startswith(
-        "`BaseEstimator._check_feature_names` is deprecated"
-    )
diff --git a/sklearn/utils/_tags.py b/sklearn/utils/_tags.py
index f63d7b3bd008c..44b3eb64523c9 100644
--- a/sklearn/utils/_tags.py
+++ b/sklearn/utils/_tags.py
@@ -1,9 +1,7 @@
 from __future__ import annotations
 
 import warnings
-from collections import OrderedDict
 from dataclasses import dataclass, field
-from itertools import chain, pairwise
 
 # Authors: The scikit-learn developers
 # SPDX-License-Identifier: BSD-3-Clause
@@ -297,71 +295,6 @@ def default_tags(estimator) -> Tags:
     )
 
 
-# TODO(1.7): Remove this function
-def _find_tags_provider(estimator, warn=True):
-    """Find the tags provider for an estimator.
-
-    Parameters
-    ----------
-    estimator : estimator object
-        The estimator to find the tags provider for.
-
-    warn : bool, default=True
-        Whether to warn if the tags provider is not found.
-
-    Returns
-    -------
-    tag_provider : str
-        The tags provider for the estimator. Can be one of:
-        - "_get_tags": to use the old tags infrastructure
-        - "__sklearn_tags__": to use the new tags infrastructure
-    """
-    mro_model = type(estimator).mro()
-    tags_mro = OrderedDict()
-    for klass in mro_model:
-        tags_provider = []
-        if "_more_tags" in vars(klass):
-            tags_provider.append("_more_tags")
-        if "_get_tags" in vars(klass):
-            tags_provider.append("_get_tags")
-        if "__sklearn_tags__" in vars(klass):
-            tags_provider.append("__sklearn_tags__")
-        tags_mro[klass.__name__] = tags_provider
-
-    all_providers = set(chain.from_iterable(tags_mro.values()))
-    if "__sklearn_tags__" not in all_providers:
-        # default on the old tags infrastructure
-        return "_get_tags"
-
-    tag_provider = "__sklearn_tags__"
-    for klass in tags_mro:
-        has_get_or_more_tags = any(
-            provider in tags_mro[klass] for provider in ("_get_tags", "_more_tags")
-        )
-        has_sklearn_tags = "__sklearn_tags__" in tags_mro[klass]
-
-        if tags_mro[klass] and tag_provider == "__sklearn_tags__":  # is it empty
-            if has_get_or_more_tags and not has_sklearn_tags:
-                # Case where a class does not implement __sklearn_tags__ and we fallback
-                # to _get_tags. We should therefore warn for implementing
-                # __sklearn_tags__.
-                tag_provider = "_get_tags"
-                break
-
-    if warn and tag_provider == "_get_tags":
-        warnings.warn(
-            f"The {estimator.__class__.__name__} or classes from which it inherits "
-            "use `_get_tags` and `_more_tags`. Please define the "
-            "`__sklearn_tags__` method, or inherit from `sklearn.base.BaseEstimator` "
-            "and/or other appropriate mixins such as `sklearn.base.TransformerMixin`, "
-            "`sklearn.base.ClassifierMixin`, `sklearn.base.RegressorMixin`, and "
-            "`sklearn.base.OutlierMixin`. From scikit-learn 1.7, not defining "
-            "`__sklearn_tags__` will raise an error.",
-            category=DeprecationWarning,
-        )
-    return tag_provider
-
-
 def get_tags(estimator) -> Tags:
     """Get estimator tags.
 
@@ -388,223 +321,35 @@ def get_tags(estimator) -> Tags:
         The estimator tags.
     """
 
-    tag_provider = _find_tags_provider(estimator)
-
-    if tag_provider == "__sklearn_tags__":
-        # TODO(1.7): turn the warning into an error
-        try:
-            tags = estimator.__sklearn_tags__()
-        except AttributeError as exc:
-            if str(exc) == "'super' object has no attribute '__sklearn_tags__'":
-                # workaround the regression reported in
-                # https://github.com/scikit-learn/scikit-learn/issues/30479
-                # `__sklearn_tags__` is implemented by calling
-                # `super().__sklearn_tags__()` but there is no `__sklearn_tags__`
-                # method in the base class.
-                warnings.warn(
-                    f"The following error was raised: {exc}. It seems that "
-                    "there are no classes that implement `__sklearn_tags__` "
-                    "in the MRO and/or all classes in the MRO call "
-                    "`super().__sklearn_tags__()`. Make sure to inherit from "
-                    "`BaseEstimator` which implements `__sklearn_tags__` (or "
-                    "alternatively define `__sklearn_tags__` but we don't recommend "
-                    "this approach). Note that `BaseEstimator` needs to be on the "
-                    "right side of other Mixins in the inheritance order. The "
-                    "default are now used instead since retrieving tags failed. "
-                    "This warning will be replaced by an error in 1.7.",
-                    category=DeprecationWarning,
-                )
-                tags = default_tags(estimator)
-            else:
-                raise
-    else:
-        # TODO(1.7): Remove this branch of the code
-        # Let's go through the MRO and patch each class implementing _more_tags
-        sklearn_tags_provider = {}
-        more_tags_provider = {}
-        class_order = []
-        for klass in reversed(type(estimator).mro()):
-            if "__sklearn_tags__" in vars(klass):
-                sklearn_tags_provider[klass] = klass.__sklearn_tags__(estimator)  # type: ignore[attr-defined]
-                class_order.append(klass)
-            elif "_more_tags" in vars(klass):
-                more_tags_provider[klass] = klass._more_tags(estimator)  # type: ignore[attr-defined]
-                class_order.append(klass)
-
-        # Find differences between consecutive in the case of __sklearn_tags__
-        # inheritance
-        sklearn_tags_diff = {}
-        items = list(sklearn_tags_provider.items())
-        for current_item, next_item in pairwise(items):
-            current_name, current_tags = current_item
-            next_name, next_tags = next_item
-            current_tags = _to_old_tags(current_tags)
-            next_tags = _to_old_tags(next_tags)
-
-            # Compare tags and store differences
-            diff = {}
-            for key in current_tags:
-                if current_tags[key] != next_tags[key]:
-                    diff[key] = next_tags[key]
-
-            sklearn_tags_diff[next_name] = diff
-
-        tags = {}
-        for klass in class_order:
-            if klass in sklearn_tags_diff:
-                tags.update(sklearn_tags_diff[klass])
-            elif klass in more_tags_provider:
-                tags.update(more_tags_provider[klass])
-
-        tags = _to_new_tags(
-            {**_to_old_tags(default_tags(estimator)), **tags}, estimator
-        )
-
-    return tags
-
-
-# TODO(1.7): Remove this function
-def _safe_tags(estimator, key=None):
-    warnings.warn(
-        "The `_safe_tags` function is deprecated in 1.6 and will be removed in "
-        "1.7. Use the public `get_tags` function instead and make sure to implement "
-        "the `__sklearn_tags__` method.",
-        category=DeprecationWarning,
-    )
-    tags = _to_old_tags(get_tags(estimator))
-
-    if key is not None:
-        if key not in tags:
-            raise ValueError(
-                f"The key {key} is not defined for the class "
-                f"{estimator.__class__.__name__}."
+    try:
+        tags = estimator.__sklearn_tags__()
+    except AttributeError as exc:
+        # TODO(1.8): turn the warning into an error
+        if "object has no attribute '__sklearn_tags__'" in str(exc):
+            # Fall back to the default tags if the estimator does not
+            # implement __sklearn_tags__.
+            # In particular, workaround the regression reported in
+            # https://github.com/scikit-learn/scikit-learn/issues/30479
+            # `__sklearn_tags__` is implemented by calling
+            # `super().__sklearn_tags__()` but there is no `__sklearn_tags__`
+            # method in the base class. Typically happens when only inheriting
+            # from Mixins.
+
+            warnings.warn(
+                f"The following error was raised: {exc}. It seems that "
+                "there are no classes that implement `__sklearn_tags__` "
+                "in the MRO and/or all classes in the MRO call "
+                "`super().__sklearn_tags__()`. Make sure to inherit from "
+                "`BaseEstimator` which implements `__sklearn_tags__` (or "
+                "alternatively define `__sklearn_tags__` but we don't recommend "
+                "this approach). Note that `BaseEstimator` needs to be on the "
+                "right side of other Mixins in the inheritance order. The "
+                "default are now used instead since retrieving tags failed. "
+                "This warning will be replaced by an error in 1.8.",
+                category=DeprecationWarning,
             )
-        return tags[key]
-    return tags
-
+            tags = default_tags(estimator)
+        else:
+            raise
 
-# TODO(1.7): Remove this function
-def _to_new_tags(old_tags, estimator=None):
-    """Utility function convert old tags (dictionary) to new tags (dataclass)."""
-    input_tags = InputTags(
-        one_d_array="1darray" in old_tags["X_types"],
-        two_d_array="2darray" in old_tags["X_types"],
-        three_d_array="3darray" in old_tags["X_types"],
-        sparse="sparse" in old_tags["X_types"],
-        categorical="categorical" in old_tags["X_types"],
-        string="string" in old_tags["X_types"],
-        dict="dict" in old_tags["X_types"],
-        positive_only=old_tags["requires_positive_X"],
-        allow_nan=old_tags["allow_nan"],
-        pairwise=old_tags["pairwise"],
-    )
-    target_tags = TargetTags(
-        required=old_tags["requires_y"],
-        one_d_labels="1dlabels" in old_tags["X_types"],
-        two_d_labels="2dlabels" in old_tags["X_types"],
-        positive_only=old_tags["requires_positive_y"],
-        multi_output=old_tags["multioutput"] or old_tags["multioutput_only"],
-        single_output=not old_tags["multioutput_only"],
-    )
-    if estimator is not None and (
-        hasattr(estimator, "transform") or hasattr(estimator, "fit_transform")
-    ):
-        transformer_tags = TransformerTags(
-            preserves_dtype=old_tags["preserves_dtype"],
-        )
-    else:
-        transformer_tags = None
-    estimator_type = getattr(estimator, "_estimator_type", None)
-    if estimator_type == "classifier":
-        classifier_tags = ClassifierTags(
-            poor_score=old_tags["poor_score"],
-            multi_class=not old_tags["binary_only"],
-            multi_label=old_tags["multilabel"],
-        )
-    else:
-        classifier_tags = None
-    if estimator_type == "regressor":
-        regressor_tags = RegressorTags(
-            poor_score=old_tags["poor_score"],
-        )
-    else:
-        regressor_tags = None
-    return Tags(
-        estimator_type=estimator_type,
-        target_tags=target_tags,
-        transformer_tags=transformer_tags,
-        classifier_tags=classifier_tags,
-        regressor_tags=regressor_tags,
-        input_tags=input_tags,
-        array_api_support=old_tags["array_api_support"],
-        no_validation=old_tags["no_validation"],
-        non_deterministic=old_tags["non_deterministic"],
-        requires_fit=old_tags["requires_fit"],
-        _skip_test=old_tags["_skip_test"],
-    )
-
-
-# TODO(1.7): Remove this function
-def _to_old_tags(new_tags):
-    """Utility function convert old tags (dictionary) to new tags (dataclass)."""
-    if new_tags.classifier_tags:
-        binary_only = not new_tags.classifier_tags.multi_class
-        multilabel = new_tags.classifier_tags.multi_label
-        poor_score_clf = new_tags.classifier_tags.poor_score
-    else:
-        binary_only = False
-        multilabel = False
-        poor_score_clf = False
-
-    if new_tags.regressor_tags:
-        poor_score_reg = new_tags.regressor_tags.poor_score
-    else:
-        poor_score_reg = False
-
-    if new_tags.transformer_tags:
-        preserves_dtype = new_tags.transformer_tags.preserves_dtype
-    else:
-        preserves_dtype = ["float64"]
-
-    tags = {
-        "allow_nan": new_tags.input_tags.allow_nan,
-        "array_api_support": new_tags.array_api_support,
-        "binary_only": binary_only,
-        "multilabel": multilabel,
-        "multioutput": new_tags.target_tags.multi_output,
-        "multioutput_only": (
-            not new_tags.target_tags.single_output and new_tags.target_tags.multi_output
-        ),
-        "no_validation": new_tags.no_validation,
-        "non_deterministic": new_tags.non_deterministic,
-        "pairwise": new_tags.input_tags.pairwise,
-        "preserves_dtype": preserves_dtype,
-        "poor_score": poor_score_clf or poor_score_reg,
-        "requires_fit": new_tags.requires_fit,
-        "requires_positive_X": new_tags.input_tags.positive_only,
-        "requires_y": new_tags.target_tags.required,
-        "requires_positive_y": new_tags.target_tags.positive_only,
-        "_skip_test": new_tags._skip_test,
-        "stateless": new_tags.requires_fit,
-    }
-    X_types = []
-    if new_tags.input_tags.one_d_array:
-        X_types.append("1darray")
-    if new_tags.input_tags.two_d_array:
-        X_types.append("2darray")
-    if new_tags.input_tags.three_d_array:
-        X_types.append("3darray")
-    if new_tags.input_tags.sparse:
-        X_types.append("sparse")
-    if new_tags.input_tags.categorical:
-        X_types.append("categorical")
-    if new_tags.input_tags.string:
-        X_types.append("string")
-    if new_tags.input_tags.dict:
-        X_types.append("dict")
-    if new_tags.target_tags.one_d_labels:
-        X_types.append("1dlabels")
-    if new_tags.target_tags.two_d_labels:
-        X_types.append("2dlabels")
-    tags["X_types"] = X_types
     return tags
diff --git a/sklearn/utils/tests/test_tags.py b/sklearn/utils/tests/test_tags.py
index 88d5593e26d47..f08dfad1a2fb1 100644
--- a/sklearn/utils/tests/test_tags.py
+++ b/sklearn/utils/tests/test_tags.py
@@ -11,15 +11,9 @@
 )
 from sklearn.pipeline import Pipeline
 from sklearn.utils import (
-    ClassifierTags,
-    InputTags,
-    RegressorTags,
     Tags,
-    TargetTags,
-    TransformerTags,
     get_tags,
 )
-from sklearn.utils._tags import _safe_tags, _to_new_tags, _to_old_tags, default_tags
 from sklearn.utils.estimator_checks import (
     check_estimator_tags_renamed,
     check_valid_tag_types,
@@ -43,8 +37,9 @@ class EmptyRegressor(RegressorMixin, BaseEstimator):
     pass
 
 
+# TODO(1.8): Update when implementing __sklearn_tags__ is required
 @pytest.mark.filterwarnings(
-    "ignore:.*no __sklearn_tags__ attribute.*:DeprecationWarning"
+    "ignore:.*no attribute '__sklearn_tags__'.*:DeprecationWarning"
 )
 @pytest.mark.parametrize(
     "estimator, value",
@@ -94,563 +89,22 @@ def __sklearn_tags__(self):
     check_valid_tag_types("MyEstimator", MyEstimator())
 
 
-########################################################################################
-# Test for the deprecation
-# TODO(1.7): Remove this
-########################################################################################
-
-
-class MixinAllowNanOldTags:
-    def _more_tags(self):
-        return {"allow_nan": True}
-
-
-class MixinAllowNanNewTags:
-    def __sklearn_tags__(self):
-        tags = super().__sklearn_tags__()
-        tags.input_tags.allow_nan = True
-        return tags
-
-
-class MixinAllowNanOldNewTags:
-    def _more_tags(self):
-        return {"allow_nan": True}  # pragma: no cover
-
-    def __sklearn_tags__(self):
-        tags = super().__sklearn_tags__()
-        tags.input_tags.allow_nan = True
-        return tags
-
-
-class MixinArrayApiSupportOldTags:
-    def _more_tags(self):
-        return {"array_api_support": True}
-
-
-class MixinArrayApiSupportNewTags:
-    def __sklearn_tags__(self):
-        tags = super().__sklearn_tags__()
-        tags.array_api_support = True
-        return tags
-
-
-class MixinArrayApiSupportOldNewTags:
-    def _more_tags(self):
-        return {"array_api_support": True}  # pragma: no cover
-
-    def __sklearn_tags__(self):
-        tags = super().__sklearn_tags__()
-        tags.array_api_support = True
-        return tags
-
-
-class PredictorOldTags(BaseEstimator):
-    def _more_tags(self):
-        return {"requires_fit": True}
-
-
-class PredictorNewTags(BaseEstimator):
-    def __sklearn_tags__(self):
-        tags = super().__sklearn_tags__()
-        tags.requires_fit = True
-        return tags
-
-
-class PredictorOldNewTags(BaseEstimator):
-    def _more_tags(self):
-        return {"requires_fit": True}  # pragma: no cover
-
-    def __sklearn_tags__(self):
-        tags = super().__sklearn_tags__()
-        tags.requires_fit = True
-        return tags
-
-
-def test_get_tags_backward_compatibility():
-    warn_msg = "Please define the `__sklearn_tags__` method"
-
-    ####################################################################################
-    # only predictor inheriting from BaseEstimator
-    predictor_classes = [PredictorNewTags, PredictorOldNewTags, PredictorOldTags]
-    for predictor_cls in predictor_classes:
-        if predictor_cls.__name__.endswith("OldTags"):
-            with pytest.warns(DeprecationWarning, match=warn_msg):
-                tags = get_tags(predictor_cls())
-        else:
-            tags = get_tags(predictor_cls())
-        assert tags.requires_fit
-
-    ####################################################################################
-    # one mixin and one predictor all inheriting from BaseEstimator
-    predictor_classes = [PredictorNewTags, PredictorOldNewTags, PredictorOldTags]
-    allow_nan_classes = [
-        MixinAllowNanNewTags,
-        MixinAllowNanOldNewTags,
-        MixinAllowNanOldTags,
-    ]
-
-    for allow_nan_cls in allow_nan_classes:
-        for predictor_cls in predictor_classes:
-
-            class ChildClass(allow_nan_cls, predictor_cls):
-                pass
-
-            if any(
-                base_cls.__name__.endswith("OldTags")
-                for base_cls in (predictor_cls, allow_nan_cls)
-            ):
-                with pytest.warns(DeprecationWarning, match=warn_msg):
-                    tags = get_tags(ChildClass())
-            else:
-                tags = get_tags(ChildClass())
-
-            assert tags.input_tags.allow_nan
-            assert tags.requires_fit
-
-    ####################################################################################
-    # two mixins and one predictor all inheriting from BaseEstimator
-    predictor_classes = [PredictorNewTags, PredictorOldNewTags, PredictorOldTags]
-    array_api_classes = [
-        MixinArrayApiSupportNewTags,
-        MixinArrayApiSupportOldNewTags,
-        MixinArrayApiSupportOldTags,
-    ]
-    allow_nan_classes = [
-        MixinAllowNanNewTags,
-        MixinAllowNanOldNewTags,
-        MixinAllowNanOldTags,
-    ]
-
-    for predictor_cls in predictor_classes:
-        for array_api_cls in array_api_classes:
-            for allow_nan_cls in allow_nan_classes:
-
-                class ChildClass(allow_nan_cls, array_api_cls, predictor_cls):
-                    pass
-
-                if any(
-                    base_cls.__name__.endswith("OldTags")
-                    for base_cls in (predictor_cls, array_api_cls, allow_nan_cls)
-                ):
-                    with pytest.warns(DeprecationWarning, match=warn_msg):
-                        tags = get_tags(ChildClass())
-                else:
-                    tags = get_tags(ChildClass())
-
-                assert tags.input_tags.allow_nan
-                assert tags.array_api_support
-                assert tags.requires_fit
-
-
-@pytest.mark.filterwarnings(
-    "ignore:.*Please define the `__sklearn_tags__` method.*:DeprecationWarning"
-)
-def test_safe_tags_backward_compatibility():
-    warn_msg = "The `_safe_tags` function is deprecated in 1.6"
-
-    ####################################################################################
-    # only predictor inheriting from BaseEstimator
-    predictor_classes = [PredictorNewTags, PredictorOldNewTags, PredictorOldTags]
-    for predictor_cls in predictor_classes:
-        with pytest.warns(DeprecationWarning, match=warn_msg):
-            tags = _safe_tags(predictor_cls())
-        assert tags["requires_fit"]
-
-    ####################################################################################
-    # one mixin and one predictor all inheriting from BaseEstimator
-    predictor_classes = [PredictorNewTags, PredictorOldNewTags, PredictorOldTags]
-    allow_nan_classes = [
-        MixinAllowNanNewTags,
-        MixinAllowNanOldNewTags,
-        MixinAllowNanOldTags,
-    ]
-
-    for allow_nan_cls in allow_nan_classes:
-        for predictor_cls in predictor_classes:
-
-            class ChildClass(allow_nan_cls, predictor_cls):
-                pass
-
-            with pytest.warns(DeprecationWarning, match=warn_msg):
-                tags = _safe_tags(ChildClass())
-
-            assert tags["allow_nan"]
-            assert tags["requires_fit"]
-
-    ####################################################################################
-    # two mixins and one predictor all inheriting from BaseEstimator
-    predictor_classes = [PredictorNewTags, PredictorOldNewTags, PredictorOldTags]
-    array_api_classes = [
-        MixinArrayApiSupportNewTags,
-        MixinArrayApiSupportOldNewTags,
-        MixinArrayApiSupportOldTags,
-    ]
-    allow_nan_classes = [
-        MixinAllowNanNewTags,
-        MixinAllowNanOldNewTags,
-        MixinAllowNanOldTags,
-    ]
-
-    for predictor_cls in predictor_classes:
-        for array_api_cls in array_api_classes:
-            for allow_nan_cls in allow_nan_classes:
-
-                class ChildClass(allow_nan_cls, array_api_cls, predictor_cls):
-                    pass
-
-                with pytest.warns(DeprecationWarning, match=warn_msg):
-                    tags = _safe_tags(ChildClass())
-
-                assert tags["allow_nan"]
-                assert tags["array_api_support"]
-                assert tags["requires_fit"]
-
-
-@pytest.mark.filterwarnings(
-    "ignore:.*Please define the `__sklearn_tags__` method.*:DeprecationWarning"
-)
-def test__get_tags_backward_compatibility():
-    warn_msg = "The `_get_tags` method is deprecated in 1.6"
-
-    ####################################################################################
-    # only predictor inheriting from BaseEstimator
-    predictor_classes = [PredictorNewTags, PredictorOldNewTags, PredictorOldTags]
-    for predictor_cls in predictor_classes:
-        with pytest.warns(DeprecationWarning, match=warn_msg):
-            tags = predictor_cls()._get_tags()
-        assert tags["requires_fit"]
-
-    ####################################################################################
-    # one mixin and one predictor all inheriting from BaseEstimator
-    predictor_classes = [PredictorNewTags, PredictorOldNewTags, PredictorOldTags]
-    allow_nan_classes = [
-        MixinAllowNanNewTags,
-        MixinAllowNanOldNewTags,
-        MixinAllowNanOldTags,
-    ]
-
-    for allow_nan_cls in allow_nan_classes:
-        for predictor_cls in predictor_classes:
-
-            class ChildClass(allow_nan_cls, predictor_cls):
-                pass
-
-            with pytest.warns(DeprecationWarning, match=warn_msg):
-                tags = ChildClass()._get_tags()
-
-            assert tags["allow_nan"]
-            assert tags["requires_fit"]
-
-    ####################################################################################
-    # two mixins and one predictor all inheriting from BaseEstimator
-    predictor_classes = [PredictorNewTags, PredictorOldNewTags, PredictorOldTags]
-    array_api_classes = [
-        MixinArrayApiSupportNewTags,
-        MixinArrayApiSupportOldNewTags,
-        MixinArrayApiSupportOldTags,
-    ]
-    allow_nan_classes = [
-        MixinAllowNanNewTags,
-        MixinAllowNanOldNewTags,
-        MixinAllowNanOldTags,
-    ]
-
-    for predictor_cls in predictor_classes:
-        for array_api_cls in array_api_classes:
-            for allow_nan_cls in allow_nan_classes:
-
-                class ChildClass(allow_nan_cls, array_api_cls, predictor_cls):
-                    pass
-
-                with pytest.warns(DeprecationWarning, match=warn_msg):
-                    tags = ChildClass()._get_tags()
-
-                assert tags["allow_nan"]
-                assert tags["array_api_support"]
-                assert tags["requires_fit"]
-
-
-def test_roundtrip_tags():
-    estimator = PredictorNewTags()
-    tags = default_tags(estimator)
-    assert _to_new_tags(_to_old_tags(tags), estimator=estimator) == tags
-
-
-def test_base_estimator_more_tags():
-    """Test that the `_more_tags` and `_get_tags` methods are equivalent for
-    `BaseEstimator`.
-    """
-    estimator = BaseEstimator()
-    with pytest.warns(
-        DeprecationWarning, match="The `_more_tags` method is deprecated"
-    ):
-        more_tags = BaseEstimator._more_tags(estimator)
-
-    with pytest.warns(DeprecationWarning, match="The `_get_tags` method is deprecated"):
-        get_tags = BaseEstimator._get_tags(estimator)
-
-    assert more_tags == get_tags
-
-
-def test_safe_tags():
-    estimator = PredictorNewTags()
-    with pytest.warns(
-        DeprecationWarning, match="The `_safe_tags` function is deprecated"
-    ):
-        tags = _safe_tags(estimator)
-
-    with pytest.warns(
-        DeprecationWarning, match="The `_safe_tags` function is deprecated"
-    ):
-        tags_requires_fit = _safe_tags(estimator, key="requires_fit")
-
-    assert tags_requires_fit == tags["requires_fit"]
-
-    err_msg = "The key unknown_key is not defined"
-    with pytest.raises(ValueError, match=err_msg):
-        with pytest.warns(
-            DeprecationWarning, match="The `_safe_tags` function is deprecated"
-        ):
-            _safe_tags(estimator, key="unknown_key")
-
-
-def test_old_tags():
-    """Set to non-default values and check that we get the expected old tags."""
-
-    class MyClass:
-        _estimator_type = "regressor"
-
-        def __sklearn_tags__(self):
-            input_tags = InputTags(
-                one_d_array=True,
-                two_d_array=False,
-                three_d_array=True,
-                sparse=True,
-                categorical=True,
-                string=True,
-                dict=True,
-                positive_only=True,
-                allow_nan=True,
-                pairwise=True,
-            )
-            target_tags = TargetTags(
-                required=False,
-                one_d_labels=True,
-                two_d_labels=True,
-                positive_only=True,
-                multi_output=True,
-                single_output=False,
-            )
-            transformer_tags = None
-            classifier_tags = None
-            regressor_tags = RegressorTags(
-                poor_score=True,
-            )
-            return Tags(
-                estimator_type=self._estimator_type,
-                input_tags=input_tags,
-                target_tags=target_tags,
-                transformer_tags=transformer_tags,
-                classifier_tags=classifier_tags,
-                regressor_tags=regressor_tags,
-            )
-
-    estimator = MyClass()
-    new_tags = get_tags(estimator)
-    old_tags = _to_old_tags(new_tags)
-    expected_tags = {
-        "allow_nan": True,
-        "array_api_support": False,
-        "binary_only": False,
-        "multilabel": False,
-        "multioutput": True,
-        "multioutput_only": True,
-        "no_validation": False,
-        "non_deterministic": False,
-        "pairwise": True,
-        "preserves_dtype": ["float64"],
-        "poor_score": True,
-        "requires_fit": True,
-        "requires_positive_X": True,
-        "requires_y": False,
-        "requires_positive_y": True,
-        "_skip_test": False,
-        "stateless": True,
-        "X_types": [
-            "1darray",
-            "3darray",
-            "sparse",
-            "categorical",
-            "string",
-            "dict",
-            "1dlabels",
-            "2dlabels",
-        ],
-    }
-    assert old_tags == expected_tags
-    assert _to_new_tags(_to_old_tags(new_tags), estimator=estimator) == new_tags
-
-    class MyClass:
-        _estimator_type = "classifier"
-
-        def __sklearn_tags__(self):
-            input_tags = InputTags(
-                one_d_array=True,
-                two_d_array=False,
-                three_d_array=True,
-                sparse=True,
-                categorical=True,
-                string=True,
-                dict=True,
-                positive_only=True,
-                allow_nan=True,
-                pairwise=True,
-            )
-            target_tags = TargetTags(
-                required=False,
-                one_d_labels=True,
-                two_d_labels=False,
-                positive_only=True,
-                multi_output=True,
-                single_output=False,
-            )
-            transformer_tags = None
-            classifier_tags = ClassifierTags(
-                poor_score=True,
-                multi_class=False,
-                multi_label=True,
-            )
-            regressor_tags = None
-            return Tags(
-                estimator_type=self._estimator_type,
-                input_tags=input_tags,
-                target_tags=target_tags,
-                transformer_tags=transformer_tags,
-                classifier_tags=classifier_tags,
-                regressor_tags=regressor_tags,
-            )
-
-    estimator = MyClass()
-    new_tags = get_tags(estimator)
-    old_tags = _to_old_tags(new_tags)
-    expected_tags = {
-        "allow_nan": True,
-        "array_api_support": False,
-        "binary_only": True,
-        "multilabel": True,
-        "multioutput": True,
-        "multioutput_only": True,
-        "no_validation": False,
-        "non_deterministic": False,
-        "pairwise": True,
-        "preserves_dtype": ["float64"],
-        "poor_score": True,
-        "requires_fit": True,
-        "requires_positive_X": True,
-        "requires_y": False,
-        "requires_positive_y": True,
-        "_skip_test": False,
-        "stateless": True,
-        "X_types": [
-            "1darray",
-            "3darray",
-            "sparse",
-            "categorical",
-            "string",
-            "dict",
-            "1dlabels",
-        ],
-    }
-    assert old_tags == expected_tags
-    assert _to_new_tags(_to_old_tags(new_tags), estimator=estimator) == new_tags
-
-    class MyClass:
-        def fit(self, X, y=None):
-            return self  # pragma: no cover
-
-        def transform(self, X):
-            return X  # pragma: no cover
-
-        def __sklearn_tags__(self):
-            input_tags = InputTags(
-                one_d_array=True,
-                two_d_array=False,
-                three_d_array=True,
-                sparse=True,
-                categorical=True,
-                string=True,
-                dict=True,
-                positive_only=True,
-                allow_nan=True,
-                pairwise=True,
-            )
-            target_tags = TargetTags(
-                required=False,
-                one_d_labels=True,
-                two_d_labels=False,
-                positive_only=True,
-                multi_output=True,
-                single_output=False,
-            )
-            transformer_tags = TransformerTags(
-                preserves_dtype=["float64"],
-            )
-            classifier_tags = None
-            regressor_tags = None
-            return Tags(
-                estimator_type=None,
-                input_tags=input_tags,
-                target_tags=target_tags,
-                transformer_tags=transformer_tags,
-                classifier_tags=classifier_tags,
-                regressor_tags=regressor_tags,
-            )
-
-    estimator = MyClass()
-    new_tags = get_tags(estimator)
-    old_tags = _to_old_tags(new_tags)
-    expected_tags = {
-        "allow_nan": True,
-        "array_api_support": False,
-        "binary_only": False,
-        "multilabel": False,
-        "multioutput": True,
-        "multioutput_only": True,
-        "no_validation": False,
-        "non_deterministic": False,
-        "pairwise": True,
-        "preserves_dtype": ["float64"],
-        "poor_score": False,
-        "requires_fit": True,
-        "requires_positive_X": True,
-        "requires_y": False,
-        "requires_positive_y": True,
-        "_skip_test": False,
-        "stateless": True,
-        "X_types": [
-            "1darray",
-            "3darray",
-            "sparse",
-            "categorical",
-            "string",
-            "dict",
-            "1dlabels",
-        ],
-    }
-    assert old_tags == expected_tags
-    assert _to_new_tags(_to_old_tags(new_tags), estimator=estimator) == new_tags
-
-
-# TODO(1.7): Remove this test
+# TODO(1.8): Update this test to check for errors
 def test_tags_no_sklearn_tags_concrete_implementation():
     """Non-regression test for:
     https://github.com/scikit-learn/scikit-learn/issues/30479
 
-    There is no class implementing `__sklearn_tags__` without calling
-    `super().__sklearn_tags__()`. Thus, we raise a warning and request to inherit from
+    Either the estimator doesn't implement `__sklearn_tags` or there is no class
+    implementing `__sklearn_tags__` without calling `super().__sklearn_tags__()` in
+    its mro. Thus, we raise a warning and request to inherit from
     `BaseEstimator` that implements `__sklearn_tags__`.
     """
 
+    X = np.array([[1, 2], [2, 3], [3, 4]])
+    y = np.array([1, 0, 1])
+
+    # 1st case, the estimator inherits from a class that only implements
+    # `__sklearn_tags__` by calling `super().__sklearn_tags__()`.
     class MyEstimator(ClassifierMixin):
         def __init__(self, *, param=1):
             self.param = param
@@ -662,16 +116,29 @@ def fit(self, X, y=None):
         def predict(self, X):
             return np.full(shape=X.shape[0], fill_value=self.param)
 
-    X = np.array([[1, 2], [2, 3], [3, 4]])
-    y = np.array([1, 0, 1])
-
     my_pipeline = Pipeline([("estimator", MyEstimator(param=1))])
     with pytest.warns(DeprecationWarning, match="The following error was raised"):
         my_pipeline.fit(X, y).predict(X)
 
+    # 2nd case, the estimator doesn't implement `__sklearn_tags__` at all.
+    class MyEstimator2:
+        def __init__(self, *, param=1):
+            self.param = param
+
+        def fit(self, X, y=None):
+            self.is_fitted_ = True
+            return self
+
+        def predict(self, X):
+            return np.full(shape=X.shape[0], fill_value=self.param)
+
+    my_pipeline = Pipeline([("estimator", MyEstimator2(param=1))])
+    with pytest.warns(DeprecationWarning, match="The following error was raised"):
+        my_pipeline.fit(X, y).predict(X)
+
     # check that we still raise an error if it is not a AttributeError or related to
     # __sklearn_tags__
-    class MyEstimator2(MyEstimator, BaseEstimator):
+    class MyEstimator3(MyEstimator, BaseEstimator):
         def __init__(self, *, param=1, error_type=AttributeError):
             self.param = param
             self.error_type = error_type
@@ -681,6 +148,6 @@ def __sklearn_tags__(self):
             raise self.error_type("test")
 
     for error_type in (AttributeError, TypeError, ValueError):
-        estimator = MyEstimator2(param=1, error_type=error_type)
+        estimator = MyEstimator3(param=1, error_type=error_type)
         with pytest.raises(error_type):
             get_tags(estimator)


Reviews:
user: login: jeremiedbb
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T15:14:19Z
user: login: jeremiedbb
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T17:09:26Z
user: login: jeremiedbb
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T17:14:13Z
user: login: adrinjalali
body: LGTM. I'll let @glemaitre have a look too.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-07T17:46:18Z
user: login: glemaitre
body: It looks good as well. I prefer this softer deprecation for one more release.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-17T11:50:25Z

Review comments:
diff_hunk: @@ -388,223 +321,35 @@ def get_tags(estimator) -> Tags:
         The estimator tags.
     """
 
-    tag_provider = _find_tags_provider(estimator)
-
-    if tag_provider == "__sklearn_tags__":
-        # TODO(1.7): turn the warning into an error
-        try:
-            tags = estimator.__sklearn_tags__()
-        except AttributeError as exc:
-            if str(exc) == "'super' object has no attribute '__sklearn_tags__'":
-                # workaround the regression reported in
-                # https://github.com/scikit-learn/scikit-learn/issues/30479
-                # `__sklearn_tags__` is implemented by calling
-                # `super().__sklearn_tags__()` but there is no `__sklearn_tags__`
-                # method in the base class.
-                warnings.warn(
-                    f"The following error was raised: {exc}. It seems that "
-                    "there are no classes that implement `__sklearn_tags__` "
-                    "in the MRO and/or all classes in the MRO call "
-                    "`super().__sklearn_tags__()`. Make sure to inherit from "
-                    "`BaseEstimator` which implements `__sklearn_tags__` (or "
-                    "alternatively define `__sklearn_tags__` but we don't recommend "
-                    "this approach). Note that `BaseEstimator` needs to be on the "
-                    "right side of other Mixins in the inheritance order. The "
-                    "default are now used instead since retrieving tags failed. "
-                    "This warning will be replaced by an error in 1.7.",
-                    category=DeprecationWarning,
-                )
-                tags = default_tags(estimator)
-            else:
-                raise
-    else:
-        # TODO(1.7): Remove this branch of the code
-        # Let's go through the MRO and patch each class implementing _more_tags
-        sklearn_tags_provider = {}
-        more_tags_provider = {}
-        class_order = []
-        for klass in reversed(type(estimator).mro()):
-            if "__sklearn_tags__" in vars(klass):
-                sklearn_tags_provider[klass] = klass.__sklearn_tags__(estimator)  # type: ignore[attr-defined]
-                class_order.append(klass)
-            elif "_more_tags" in vars(klass):
-                more_tags_provider[klass] = klass._more_tags(estimator)  # type: ignore[attr-defined]
-                class_order.append(klass)
-
-        # Find differences between consecutive in the case of __sklearn_tags__
-        # inheritance
-        sklearn_tags_diff = {}
-        items = list(sklearn_tags_provider.items())
-        for current_item, next_item in pairwise(items):
-            current_name, current_tags = current_item
-            next_name, next_tags = next_item
-            current_tags = _to_old_tags(current_tags)
-            next_tags = _to_old_tags(next_tags)
-
-            # Compare tags and store differences
-            diff = {}
-            for key in current_tags:
-                if current_tags[key] != next_tags[key]:
-                    diff[key] = next_tags[key]
-
-            sklearn_tags_diff[next_name] = diff
-
-        tags = {}
-        for klass in class_order:
-            if klass in sklearn_tags_diff:
-                tags.update(sklearn_tags_diff[klass])
-            elif klass in more_tags_provider:
-                tags.update(more_tags_provider[klass])
-
-        tags = _to_new_tags(
-            {**_to_old_tags(default_tags(estimator)), **tags}, estimator
-        )
-
-    return tags
-
-
-# TODO(1.7): Remove this function
-def _safe_tags(estimator, key=None):
-    warnings.warn(
-        "The `_safe_tags` function is deprecated in 1.6 and will be removed in "
-        "1.7. Use the public `get_tags` function instead and make sure to implement "
-        "the `__sklearn_tags__` method.",
-        category=DeprecationWarning,
-    )
-    tags = _to_old_tags(get_tags(estimator))
-
-    if key is not None:
-        if key not in tags:
-            raise ValueError(
-                f"The key {key} is not defined for the class "
-                f"{estimator.__class__.__name__}."
+    try:
+        tags = estimator.__sklearn_tags__()
+    except AttributeError as exc:
+        # TODO(1.8): turn the warning into an error
+        if "object has no attribute '__sklearn_tags__'" in str(exc):
+            # Fall back to the default tags if the estimator does not
+            # implement __sklearn_tags__.
+            # In particular, workaround the regression reported in
+            # https://github.com/scikit-learn/scikit-learn/issues/30479
+            # `__sklearn_tags__` is implemented by calling
+            # `super().__sklearn_tags__()` but there is no `__sklearn_tags__`
+            # method in the base class. Typically happens when only inheriting
+            # from Mixins.
+
+            warnings.warn(
+                f"The following error was raised: {exc}. It seems that "
+                "there are no classes that implement `__sklearn_tags__` "
+                "in the MRO and/or all classes in the MRO call "
+                "`super().__sklearn_tags__()`. Make sure to inherit from "
+                "`BaseEstimator` which implements `__sklearn_tags__` (or "
+                "alternatively define `__sklearn_tags__` but we don't recommend "
+                "this approach). Note that `BaseEstimator` needs to be on the "
+                "right side of other Mixins in the inheritance order. The "
+                "default are now used instead since retrieving tags failed. "
+                "This warning will be replaced by an error in 1.8.",
+                category=DeprecationWarning,
path: sklearn/utils/_tags.py
user: login: jeremiedbb
body: I did not follow the original plan here see my following comment
created_at: 2025-04-02T15:14:19Z
author_association: MEMBER
diff_hunk: @@ -1221,7 +1221,7 @@ def __sklearn_tags__(self):
             tags.input_tags.sparse = all(
                 get_tags(step).input_tags.sparse
                 for name, step in self.steps
-                if step != "passthrough"
+                if step is not None and step != "passthrough"
path: sklearn/pipeline.py
user: login: jeremiedbb
body: That was missed because `get_tags(None)` used to return default tags.
created_at: 2025-04-02T17:09:26Z
author_association: MEMBER
diff_hunk: @@ -43,8 +37,9 @@ class EmptyRegressor(RegressorMixin, BaseEstimator):
     pass
 
 
+# TODO(1.8): Update when implementing __sklearn_tags__ is required
 @pytest.mark.filterwarnings(
-    "ignore:.*no __sklearn_tags__ attribute.*:DeprecationWarning"
+    "ignore:.*no attribute '__sklearn_tags__'.*:DeprecationWarning"
 )
 @pytest.mark.parametrize(
     "estimator, value",
path: sklearn/utils/tests/test_tags.py
user: login: jeremiedbb
body: This filter used to not filter anything but didn't have to :)
Now that I've made it so that a warning is raised for estimator not implementing `__sklearn_tags__`, it triggers and showed that the filter was wrong.
created_at: 2025-04-02T17:14:13Z
author_association: MEMBER
diff_hunk: @@ -388,223 +321,35 @@ def get_tags(estimator) -> Tags:
         The estimator tags.
     """
 
-    tag_provider = _find_tags_provider(estimator)
-
-    if tag_provider == "__sklearn_tags__":
-        # TODO(1.7): turn the warning into an error
-        try:
-            tags = estimator.__sklearn_tags__()
-        except AttributeError as exc:
-            if str(exc) == "'super' object has no attribute '__sklearn_tags__'":
-                # workaround the regression reported in
-                # https://github.com/scikit-learn/scikit-learn/issues/30479
-                # `__sklearn_tags__` is implemented by calling
-                # `super().__sklearn_tags__()` but there is no `__sklearn_tags__`
-                # method in the base class.
-                warnings.warn(
-                    f"The following error was raised: {exc}. It seems that "
-                    "there are no classes that implement `__sklearn_tags__` "
-                    "in the MRO and/or all classes in the MRO call "
-                    "`super().__sklearn_tags__()`. Make sure to inherit from "
-                    "`BaseEstimator` which implements `__sklearn_tags__` (or "
-                    "alternatively define `__sklearn_tags__` but we don't recommend "
-                    "this approach). Note that `BaseEstimator` needs to be on the "
-                    "right side of other Mixins in the inheritance order. The "
-                    "default are now used instead since retrieving tags failed. "
-                    "This warning will be replaced by an error in 1.7.",
-                    category=DeprecationWarning,
-                )
-                tags = default_tags(estimator)
-            else:
-                raise
-    else:
-        # TODO(1.7): Remove this branch of the code
-        # Let's go through the MRO and patch each class implementing _more_tags
-        sklearn_tags_provider = {}
-        more_tags_provider = {}
-        class_order = []
-        for klass in reversed(type(estimator).mro()):
-            if "__sklearn_tags__" in vars(klass):
-                sklearn_tags_provider[klass] = klass.__sklearn_tags__(estimator)  # type: ignore[attr-defined]
-                class_order.append(klass)
-            elif "_more_tags" in vars(klass):
-                more_tags_provider[klass] = klass._more_tags(estimator)  # type: ignore[attr-defined]
-                class_order.append(klass)
-
-        # Find differences between consecutive in the case of __sklearn_tags__
-        # inheritance
-        sklearn_tags_diff = {}
-        items = list(sklearn_tags_provider.items())
-        for current_item, next_item in pairwise(items):
-            current_name, current_tags = current_item
-            next_name, next_tags = next_item
-            current_tags = _to_old_tags(current_tags)
-            next_tags = _to_old_tags(next_tags)
-
-            # Compare tags and store differences
-            diff = {}
-            for key in current_tags:
-                if current_tags[key] != next_tags[key]:
-                    diff[key] = next_tags[key]
-
-            sklearn_tags_diff[next_name] = diff
-
-        tags = {}
-        for klass in class_order:
-            if klass in sklearn_tags_diff:
-                tags.update(sklearn_tags_diff[klass])
-            elif klass in more_tags_provider:
-                tags.update(more_tags_provider[klass])
-
-        tags = _to_new_tags(
-            {**_to_old_tags(default_tags(estimator)), **tags}, estimator
-        )
-
-    return tags
-
-
-# TODO(1.7): Remove this function
-def _safe_tags(estimator, key=None):
-    warnings.warn(
-        "The `_safe_tags` function is deprecated in 1.6 and will be removed in "
-        "1.7. Use the public `get_tags` function instead and make sure to implement "
-        "the `__sklearn_tags__` method.",
-        category=DeprecationWarning,
-    )
-    tags = _to_old_tags(get_tags(estimator))
-
-    if key is not None:
-        if key not in tags:
-            raise ValueError(
-                f"The key {key} is not defined for the class "
-                f"{estimator.__class__.__name__}."
+    try:
+        tags = estimator.__sklearn_tags__()
+    except AttributeError as exc:
+        # TODO(1.8): turn the warning into an error
+        if "object has no attribute '__sklearn_tags__'" in str(exc):
+            # Fall back to the default tags if the estimator does not
+            # implement __sklearn_tags__.
+            # In particular, workaround the regression reported in
+            # https://github.com/scikit-learn/scikit-learn/issues/30479
+            # `__sklearn_tags__` is implemented by calling
+            # `super().__sklearn_tags__()` but there is no `__sklearn_tags__`
+            # method in the base class. Typically happens when only inheriting
+            # from Mixins.
+
+            warnings.warn(
+                f"The following error was raised: {exc}. It seems that "
+                "there are no classes that implement `__sklearn_tags__` "
+                "in the MRO and/or all classes in the MRO call "
+                "`super().__sklearn_tags__()`. Make sure to inherit from "
+                "`BaseEstimator` which implements `__sklearn_tags__` (or "
+                "alternatively define `__sklearn_tags__` but we don't recommend "
+                "this approach). Note that `BaseEstimator` needs to be on the "
+                "right side of other Mixins in the inheritance order. The "
+                "default are now used instead since retrieving tags failed. "
+                "This warning will be replaced by an error in 1.8.",
+                category=DeprecationWarning,
path: sklearn/utils/_tags.py
user: login: adrinjalali
body: Interesting approach. I don't mind.
created_at: 2025-04-07T17:46:04Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-02T15:14:41Z
body: ## ✔️ Linting Passed
All linting checks passed. Your pull request is in excellent shape! ☀️

<sub> _Generated for commit: [de801a4](https://github.com/scikit-learn/scikit-learn/pull/31134/commits/de801a4611291491dc8ccff45c9179d2b6d0bc4f). Link to the linter CI: [here](https://github.com/scikit-learn/scikit-learn/actions/runs/14493800557)_ </sub>
author_association: NONE
user: login: jeremiedbb
created_at: 2025-04-02T15:21:02Z
body: In `get_tags` we added a deprecation warning for cases where inherit from classes that implement `__sklearn_tags__` only calling `super().__sklearn_tags__`. However we did nothing for classes that don't implement `__sklearn_tags__` tags at all.

So the following doesn't raise any warning in main:
```py
import numpy as np
from sklearn.pipeline import Pipeline


class MyEstimator:
    def __init__(self, *, param=1):
        self.param = param

    def fit(self, X, y=None):
        self.is_fitted_ = True
        return self

    def predict(self, X):
        return np.full(shape=X.shape[0], fill_value=self.param)

my_pipeline = Pipeline([("estimator", MyEstimator(param=1))])

X = np.array([[1, 2], [2, 3], [3, 4]])
y = np.array([1, 0, 1])

my_pipeline.fit(X, y).predict(X)
```

Cleaning-up `get_tags` according to the plan would raise an error in 1.7 which I find not very user friendly. So I think it's better to raise the warning for this case as well for at least 1 more release. What do you think @glemaitre @adrinjalali ?
author_association: MEMBER