diff --git a/doc/modules/random_projection.rst b/doc/modules/random_projection.rst
index adb2d53bd14d6..7d3341f0244bb 100644
--- a/doc/modules/random_projection.rst
+++ b/doc/modules/random_projection.rst
@@ -160,3 +160,42 @@ projection transformer::
    In Proceedings of the 12th ACM SIGKDD international conference on
    Knowledge discovery and data mining (KDD '06). ACM, New York, NY, USA,
    287-296.
+
+
+.. _random_projection_inverse_transform:
+
+Inverse Transform
+=================
+The random projection transformers have ``compute_inverse_components`` parameter. When
+set to True, after creating the random ``components_`` matrix during fitting,
+the transformer computes the pseudo-inverse of this matrix and stores it as
+``inverse_components_``. The ``inverse_components_`` matrix has shape
+:math:`n_{features} \times n_{components}`, and it is always a dense matrix,
+regardless of whether the components matrix is sparse or dense. So depending on
+the number of features and components, it may use a lot of memory.
+
+When the ``inverse_transform`` method is called, it computes the product of the
+input ``X`` and the transpose of the inverse components. If the inverse components have
+been computed during fit, they are reused at each call to ``inverse_transform``.
+Otherwise they are recomputed each time, which can be costly. The result is always
+dense, even if ``X`` is sparse.
+
+Here a small code example which illustrates how to use the inverse transform
+feature::
+
+  >>> import numpy as np
+  >>> from sklearn.random_projection import SparseRandomProjection
+  >>> X = np.random.rand(100, 10000)
+  >>> transformer = SparseRandomProjection(
+  ...   compute_inverse_components=True
+  ... )
+  ...
+  >>> X_new = transformer.fit_transform(X)
+  >>> X_new.shape
+  (100, 3947)
+  >>> X_new_inversed = transformer.inverse_transform(X_new)
+  >>> X_new_inversed.shape
+  (100, 10000)
+  >>> X_new_again = transformer.transform(X_new_inversed)
+  >>> np.allclose(X_new, X_new_again)
+  True
diff --git a/doc/whats_new/v1.1.rst b/doc/whats_new/v1.1.rst
index 5030ed8faa4a1..e66640cfd2d21 100644
--- a/doc/whats_new/v1.1.rst
+++ b/doc/whats_new/v1.1.rst
@@ -791,6 +791,14 @@ Changelog
   :class:`random_projection.GaussianRandomProjection` preserves dtype for
   `numpy.float32`. :pr:`22114` by :user:`Takeshi Oura <takoika>`.

+- |Enhancement| Adds an :meth:`inverse_transform` method and a
+  `compute_inverse_transform` parameter to all transformers in the
+  :mod:`~sklearn.random_projection` module:
+  :class:`~sklearn.random_projection.GaussianRandomProjection` and
+  :class:`~sklearn.random_projection.SparseRandomProjection`. When the parameter is set
+  to True, the pseudo-inverse of the components is computed during `fit` and stored as
+  `inverse_components_`. :pr:`21701` by `Aurélien Geron <ageron>`.
+
 - |API| Adds :term:`get_feature_names_out` to all transformers in the
   :mod:`~sklearn.random_projection` module:
   :class:`~sklearn.random_projection.GaussianRandomProjection` and
diff --git a/sklearn/random_projection.py b/sklearn/random_projection.py
index 31ebfdddd8928..000eca478553e 100644
--- a/sklearn/random_projection.py
+++ b/sklearn/random_projection.py
@@ -31,6 +31,7 @@
 from abc import ABCMeta, abstractmethod

 import numpy as np
+from scipy import linalg
 import scipy.sparse as sp

 from .base import BaseEstimator, TransformerMixin
@@ -39,10 +40,9 @@
 from .utils import check_random_state
 from .utils.extmath import safe_sparse_dot
 from .utils.random import sample_without_replacement
-from .utils.validation import check_is_fitted
+from .utils.validation import check_array, check_is_fitted
 from .exceptions import DataDimensionalityWarning

-
 __all__ = [
     "SparseRandomProjection",
     "GaussianRandomProjection",
@@ -302,11 +302,18 @@ class BaseRandomProjection(

     @abstractmethod
     def __init__(
-        self, n_components="auto", *, eps=0.1, dense_output=False, random_state=None
+        self,
+        n_components="auto",
+        *,
+        eps=0.1,
+        dense_output=False,
+        compute_inverse_components=False,
+        random_state=None,
     ):
         self.n_components = n_components
         self.eps = eps
         self.dense_output = dense_output
+        self.compute_inverse_components = compute_inverse_components
         self.random_state = random_state

     @abstractmethod
@@ -323,12 +330,18 @@ def _make_random_matrix(self, n_components, n_features):

         Returns
         -------
-        components : {ndarray, sparse matrix} of shape \
-                (n_components, n_features)
+        components : {ndarray, sparse matrix} of shape (n_components, n_features)
             The generated random matrix. Sparse matrix will be of CSR format.

         """

+    def _compute_inverse_components(self):
+        """Compute the pseudo-inverse of the (densified) components."""
+        components = self.components_
+        if sp.issparse(components):
+            components = components.toarray()
+        return linalg.pinv(components, check_finite=False)
+
     def fit(self, X, y=None):
         """Generate a sparse random projection matrix.

@@ -399,6 +412,9 @@ def fit(self, X, y=None):
             " not the proper shape."
         )

+        if self.compute_inverse_components:
+            self.inverse_components_ = self._compute_inverse_components()
+
         return self

     def transform(self, X):
@@ -437,6 +453,35 @@ def _n_features_out(self):
         """
         return self.n_components

+    def inverse_transform(self, X):
+        """Project data back to its original space.
+
+        Returns an array X_original whose transform would be X. Note that even
+        if X is sparse, X_original is dense: this may use a lot of RAM.
+
+        If `compute_inverse_components` is False, the inverse of the components is
+        computed during each call to `inverse_transform` which can be costly.
+
+        Parameters
+        ----------
+        X : {array-like, sparse matrix} of shape (n_samples, n_components)
+            Data to be transformed back.
+
+        Returns
+        -------
+        X_original : ndarray of shape (n_samples, n_features)
+            Reconstructed data.
+        """
+        check_is_fitted(self)
+
+        X = check_array(X, dtype=[np.float64, np.float32], accept_sparse=("csr", "csc"))
+
+        if self.compute_inverse_components:
+            return X @ self.inverse_components_.T
+
+        inverse_components = self._compute_inverse_components()
+        return X @ inverse_components.T
+
     def _more_tags(self):
         return {
             "preserves_dtype": [np.float64, np.float32],
@@ -474,6 +519,11 @@ class GaussianRandomProjection(BaseRandomProjection):
         Smaller values lead to better embedding and higher number of
         dimensions (n_components) in the target projection space.

+    compute_inverse_components : bool, default=False
+        Learn the inverse transform by computing the pseudo-inverse of the
+        components during fit. Note that computing the pseudo-inverse does not
+        scale well to large matrices.
+
     random_state : int, RandomState instance or None, default=None
         Controls the pseudo random number generator used to generate the
         projection matrix at fit time.
@@ -488,6 +538,12 @@ class GaussianRandomProjection(BaseRandomProjection):
     components_ : ndarray of shape (n_components, n_features)
         Random matrix used for the projection.

+    inverse_components_ : ndarray of shape (n_features, n_components)
+        Pseudo-inverse of the components, only computed if
+        `compute_inverse_components` is True.
+
+        .. versionadded:: 1.1
+
     n_features_in_ : int
         Number of features seen during :term:`fit`.

@@ -516,11 +572,19 @@ class GaussianRandomProjection(BaseRandomProjection):
     (25, 2759)
     """

-    def __init__(self, n_components="auto", *, eps=0.1, random_state=None):
+    def __init__(
+        self,
+        n_components="auto",
+        *,
+        eps=0.1,
+        compute_inverse_components=False,
+        random_state=None,
+    ):
         super().__init__(
             n_components=n_components,
             eps=eps,
             dense_output=True,
+            compute_inverse_components=compute_inverse_components,
             random_state=random_state,
         )

@@ -610,6 +674,14 @@ class SparseRandomProjection(BaseRandomProjection):
         If False, the projected data uses a sparse representation if
         the input is sparse.

+    compute_inverse_components : bool, default=False
+        Learn the inverse transform by computing the pseudo-inverse of the
+        components during fit. Note that the pseudo-inverse is always a dense
+        array, even if the training data was sparse. This means that it might be
+        necessary to call `inverse_transform` on a small batch of samples at a
+        time to avoid exhausting the available memory on the host. Moreover,
+        computing the pseudo-inverse does not scale well to large matrices.
+
     random_state : int, RandomState instance or None, default=None
         Controls the pseudo random number generator used to generate the
         projection matrix at fit time.
@@ -625,6 +697,12 @@ class SparseRandomProjection(BaseRandomProjection):
         Random matrix used for the projection. Sparse matrix will be of CSR
         format.

+    inverse_components_ : ndarray of shape (n_features, n_components)
+        Pseudo-inverse of the components, only computed if
+        `compute_inverse_components` is True.
+
+        .. versionadded:: 1.1
+
     density_ : float in range 0.0 - 1.0
         Concrete density computed from when density = "auto".

@@ -676,12 +754,14 @@ def __init__(
         density="auto",
         eps=0.1,
         dense_output=False,
+        compute_inverse_components=False,
         random_state=None,
     ):
         super().__init__(
             n_components=n_components,
             eps=eps,
             dense_output=dense_output,
+            compute_inverse_components=compute_inverse_components,
             random_state=random_state,
         )

diff --git a/sklearn/tests/test_random_projection.py b/sklearn/tests/test_random_projection.py
index a3a6b1ae2a49f..4d21090a3e6fb 100644
--- a/sklearn/tests/test_random_projection.py
+++ b/sklearn/tests/test_random_projection.py
@@ -1,5 +1,6 @@
 import functools
 from typing import List, Any
+import warnings

 import numpy as np
 import scipy.sparse as sp
@@ -31,8 +32,8 @@

 # Make some random data with uniformly located non zero entries with
 # Gaussian distributed values
-def make_sparse_random_data(n_samples, n_features, n_nonzeros):
-    rng = np.random.RandomState(0)
+def make_sparse_random_data(n_samples, n_features, n_nonzeros, random_state=0):
+    rng = np.random.RandomState(random_state)
     data_coo = sp.coo_matrix(
         (
             rng.randn(n_nonzeros),
@@ -377,6 +378,57 @@ def test_random_projection_feature_names_out(random_projection_cls):
     assert_array_equal(names_out, expected_names_out)

+@pytest.mark.parametrize("n_samples", (2, 9, 10, 11, 1000))
+@pytest.mark.parametrize("n_features", (2, 9, 10, 11, 1000))
+@pytest.mark.parametrize("random_projection_cls", all_RandomProjection)
+@pytest.mark.parametrize("compute_inverse_components", [True, False])
+def test_inverse_transform(
+    n_samples,
+    n_features,
+    random_projection_cls,
+    compute_inverse_components,
+    global_random_seed,
+):
+    n_components = 10
+
+    random_projection = random_projection_cls(
+        n_components=n_components,
+        compute_inverse_components=compute_inverse_components,
+        random_state=global_random_seed,
+    )
+
+    X_dense, X_csr = make_sparse_random_data(
+        n_samples,
+        n_features,
+        n_samples * n_features // 100 + 1,
+        random_state=global_random_seed,
+    )
+
+    for X in [X_dense, X_csr]:
+        with warnings.catch_warnings():
+            warnings.filterwarnings(
+                "ignore",
+                message=(
+                    "The number of components is higher than the number of features"
+                ),
+                category=DataDimensionalityWarning,
+            )
+            projected = random_projection.fit_transform(X)
+
+        if compute_inverse_components:
+            assert hasattr(random_projection, "inverse_components_")
+            inv_components = random_projection.inverse_components_
+            assert inv_components.shape == (n_features, n_components)
+
+        projected_back = random_projection.inverse_transform(projected)
+        assert projected_back.shape == X.shape
+
+        projected_again = random_projection.transform(projected_back)
+        if hasattr(projected, "toarray"):
+            projected = projected.toarray()
+        assert_allclose(projected, projected_again, rtol=1e-7, atol=1e-10)
+
+
 @pytest.mark.parametrize("random_projection_cls", all_RandomProjection)
 @pytest.mark.parametrize(
     "input_dtype, expected_dtype",

diff --git a/doc/modules/random_projection.rst b/doc/modules/random_projection.rst
index adb2d53bd..7d3341f02 100644
--- a/doc/modules/random_projection.rst
+++ b/doc/modules/random_projection.rst
@@ -160,3 +160,42 @@ projection transformer::
    In Proceedings of the 12th ACM SIGKDD international conference on
    Knowledge discovery and data mining (KDD '06). ACM, New York, NY, USA,
    287-296.
+
+
+.. _random_projection_inverse_transform:
+
+Inverse Transform
+=================
+The random projection transformers have ``compute_inverse_components`` parameter. When
+set to True, after creating the random ``components_`` matrix during fitting,
+the transformer computes the pseudo-inverse of this matrix and stores it as
+``inverse_components_``. The ``inverse_components_`` matrix has shape
+:math:`n_{features} \times n_{components}`, and it is always a dense matrix,
+regardless of whether the components matrix is sparse or dense. So depending on
+the number of features and components, it may use a lot of memory.
+
+When the ``inverse_transform`` method is called, it computes the product of the
+input ``X`` and the transpose of the inverse components. If the inverse components have
+been computed during fit, they are reused at each call to ``inverse_transform``.
+Otherwise they are recomputed each time, which can be costly. The result is always
+dense, even if ``X`` is sparse.
+
+Here a small code example which illustrates how to use the inverse transform
+feature::
+
+  >>> import numpy as np
+  >>> from sklearn.random_projection import SparseRandomProjection
+  >>> X = np.random.rand(100, 10000)
+  >>> transformer = SparseRandomProjection(
+  ...   compute_inverse_components=True
+  ... )
+  ...
+  >>> X_new = transformer.fit_transform(X)
+  >>> X_new.shape
+  (100, 3947)
+  >>> X_new_inversed = transformer.inverse_transform(X_new)
+  >>> X_new_inversed.shape
+  (100, 10000)
+  >>> X_new_again = transformer.transform(X_new_inversed)
+  >>> np.allclose(X_new, X_new_again)
+  True

diff --git a/doc/whats_new/v1.1.rst b/doc/whats_new/v1.1.rst
index 5030ed8fa..e75c70abd 100644
--- a/doc/whats_new/v1.1.rst
+++ b/doc/whats_new/v1.1.rst
@@ -791,6 +791,14 @@ Changelog
   :class:`random_projection.GaussianRandomProjection` preserves dtype for
   `numpy.float32`. :pr:`22114` by :user:`Takeshi Oura <takoika>`.

+- |Enhancement| Adds an :meth:`inverse_transform` method and a
+  `compute_inverse_transform` parameter to all transformers in the
+  :mod:`~sklearn.random_projection` module:
+  :class:`~sklearn.random_projection.GaussianRandomProjection` and
+  :class:`~sklearn.random_projection.SparseRandomProjection`. When the parameter is set
+  to True, the pseudo-inverse of the components is computed during `fit` and stored as
+  `inverse_components_`.
+
 - |API| Adds :term:`get_feature_names_out` to all transformers in the
   :mod:`~sklearn.random_projection` module:
   :class:`~sklearn.random_projection.GaussianRandomProjection` and


diff --git a/doc/whats_new/v1.1.rst b/doc/whats_new/v1.1.rst
index 732d2e8035aa6..4c61e08c15149 100644
--- a/doc/whats_new/v1.1.rst
+++ b/doc/whats_new/v1.1.rst
@@ -636,6 +636,10 @@ Changelog
   of sample weights when the input is sparse.
   :pr:`22899` by :user:`Jérémie du Boisberranger <jeremiedbb>`.

+- |Feature| :class:`Ridge` with `solver="lsqr"` now supports to fit sparse input with
+  `fit_intercept=True`.
+  :pr:`22950` by :user:`Christian Lorentzen <lorentzenchr>`.
+
 :mod:`sklearn.manifold`
 .......................

diff --git a/sklearn/linear_model/_ridge.py b/sklearn/linear_model/_ridge.py
index 900b5f7c221db..e05f68388ffd6 100644
--- a/sklearn/linear_model/_ridge.py
+++ b/sklearn/linear_model/_ridge.py
@@ -40,6 +40,22 @@
 from ..utils.sparsefuncs import mean_variance_axis

+def _get_rescaled_operator(X, X_offset, sample_weight_sqrt):
+    """Create LinearOperator for matrix products with implicit centering.
+
+    Matrix product `LinearOperator @ coef` returns `(X - X_offset) @ coef`.
+    """
+
+    def matvec(b):
+        return X.dot(b) - sample_weight_sqrt * b.dot(X_offset)
+
+    def rmatvec(b):
+        return X.T.dot(b) - X_offset * b.dot(sample_weight_sqrt)
+
+    X1 = sparse.linalg.LinearOperator(shape=X.shape, matvec=matvec, rmatvec=rmatvec)
+    return X1
+
+
 def _solve_sparse_cg(
     X,
     y,
@@ -54,25 +70,13 @@ def _solve_sparse_cg(
     if sample_weight_sqrt is None:
         sample_weight_sqrt = np.ones(X.shape[0], dtype=X.dtype)

-    def _get_rescaled_operator(X):
-
-        X_offset_scale = X_offset / X_scale
-
-        def matvec(b):
-            return X.dot(b) - sample_weight_sqrt * b.dot(X_offset_scale)
-
-        def rmatvec(b):
-            return X.T.dot(b) - X_offset_scale * b.dot(sample_weight_sqrt)
-
-        X1 = sparse.linalg.LinearOperator(shape=X.shape, matvec=matvec, rmatvec=rmatvec)
-        return X1
-
     n_samples, n_features = X.shape

     if X_offset is None or X_scale is None:
         X1 = sp_linalg.aslinearoperator(X)
     else:
-        X1 = _get_rescaled_operator(X)
+        X_offset_scale = X_offset / X_scale
+        X1 = _get_rescaled_operator(X, X_offset_scale, sample_weight_sqrt)

     coefs = np.empty((y.shape[1], n_features), dtype=X.dtype)

@@ -137,7 +141,44 @@ def _mv(x):
     return coefs

-def _solve_lsqr(X, y, alpha, max_iter=None, tol=1e-3):
+def _solve_lsqr(
+    X,
+    y,
+    *,
+    alpha,
+    fit_intercept=True,
+    max_iter=None,
+    tol=1e-3,
+    X_offset=None,
+    X_scale=None,
+    sample_weight_sqrt=None,
+):
+    """Solve Ridge regression via LSQR.
+
+    We expect that y is always mean centered.
+    If X is dense, we expect it to be mean centered such that we can solve
+        ||y - Xw||_2^2 + alpha * ||w||_2^2
+
+    If X is sparse, we expect X_offset to be given such that we can solve
+        ||y - (X - X_offset)w||_2^2 + alpha * ||w||_2^2
+
+    With sample weights S=diag(sample_weight), this becomes
+        ||sqrt(S) (y - (X - X_offset) w)||_2^2 + alpha * ||w||_2^2
+    and we expect y and X to already be rescaled, i.e. sqrt(S) @ y, sqrt(S) @ X. In
+    this case, X_offset is the sample_weight weighted mean of X before scaling by
+    sqrt(S). The objective then reads
+       ||y - (X - sqrt(S) X_offset) w)||_2^2 + alpha * ||w||_2^2
+    """
+    if sample_weight_sqrt is None:
+        sample_weight_sqrt = np.ones(X.shape[0], dtype=X.dtype)
+
+    if sparse.issparse(X) and fit_intercept:
+        X_offset_scale = X_offset / X_scale
+        X1 = _get_rescaled_operator(X, X_offset_scale, sample_weight_sqrt)
+    else:
+        # No need to touch anything
+        X1 = X
+
     n_samples, n_features = X.shape
     coefs = np.empty((y.shape[1], n_features), dtype=X.dtype)
     n_iter = np.empty(y.shape[1], dtype=np.int32)
@@ -148,7 +189,7 @@ def _solve_lsqr(X, y, alpha, max_iter=None, tol=1e-3):
     for i in range(y.shape[1]):
         y_column = y[:, i]
         info = sp_linalg.lsqr(
-            X, y_column, damp=sqrt_alpha[i], atol=tol, btol=tol, iter_lim=max_iter
+            X1, y_column, damp=sqrt_alpha[i], atol=tol, btol=tol, iter_lim=max_iter
         )
         coefs[i] = info[0]
         n_iter[i] = info[2]
@@ -351,7 +392,7 @@ def ridge_regression(
     ----------
     X : {ndarray, sparse matrix, LinearOperator} of shape \
         (n_samples, n_features)
-        Training data
+        Training data.

     y : ndarray of shape (n_samples,) or (n_samples, n_targets)
         Target values.
@@ -409,9 +450,9 @@ def ridge_regression(
           `scipy.optimize.minimize`. It can be used only when `positive`
           is True.

-        All last six solvers support both dense and sparse data. However, only
-        'sag', 'sparse_cg', and 'lbfgs' support sparse input when `fit_intercept`
-        is True.
+        All solvers except 'svd' support both dense and sparse data. However, only
+        'lsqr', 'sag', 'sparse_cg', and 'lbfgs' support sparse input when
+        `fit_intercept` is True.

         .. versionadded:: 0.17
            Stochastic Average Gradient descent solver.
@@ -518,6 +559,7 @@ def _ridge_regression(
     X_scale=None,
     X_offset=None,
     check_input=True,
+    fit_intercept=False,
 ):

     has_sw = sample_weight is not None
@@ -629,7 +671,17 @@ def _ridge_regression(
         )

     elif solver == "lsqr":
-        coef, n_iter = _solve_lsqr(X, y, alpha, max_iter, tol)
+        coef, n_iter = _solve_lsqr(
+            X,
+            y,
+            alpha=alpha,
+            fit_intercept=fit_intercept,
+            max_iter=max_iter,
+            tol=tol,
+            X_offset=X_offset,
+            X_scale=X_scale,
+            sample_weight_sqrt=sample_weight_sqrt if has_sw else None,
+        )

     elif solver == "cholesky":
         if n_features > n_samples:
@@ -764,15 +816,15 @@ def fit(self, X, y, sample_weight=None):
             else:
                 solver = self.solver
         elif sparse.issparse(X) and self.fit_intercept:
-            if self.solver not in ["auto", "sparse_cg", "sag", "lbfgs"]:
+            if self.solver not in ["auto", "lbfgs", "lsqr", "sag", "sparse_cg"]:
                 raise ValueError(
                     "solver='{}' does not support fitting the intercept "
                     "on sparse data. Please set the solver to 'auto' or "
-                    "'sparse_cg', 'sag', 'lbfgs' "
+                    "'lsqr', 'sparse_cg', 'sag', 'lbfgs' "
                     "or set `fit_intercept=False`".format(self.solver)
                 )
-            if self.solver == "lbfgs":
-                solver = "lbfgs"
+            if self.solver in ["lsqr", "lbfgs"]:
+                solver = self.solver
             elif self.solver == "sag" and self.max_iter is None and self.tol > 1e-4:
                 warnings.warn(
                     '"sag" solver requires many iterations to fit '
@@ -846,6 +898,7 @@ def fit(self, X, y, sample_weight=None):
                 return_n_iter=True,
                 return_intercept=False,
                 check_input=False,
+                fit_intercept=self.fit_intercept,
                 **params,
             )
             self._set_intercept(X_offset, y_offset, X_scale)
@@ -944,9 +997,9 @@ class Ridge(MultiOutputMixin, RegressorMixin, _BaseRidge):
           `scipy.optimize.minimize`. It can be used only when `positive`
           is True.

-        All last six solvers support both dense and sparse data. However, only
-        'sag', 'sparse_cg', and 'lbfgs' support sparse input when `fit_intercept`
-        is True.
+        All solvers except 'svd' support both dense and sparse data. However, only
+        'lsqr', 'sag', 'sparse_cg', and 'lbfgs' support sparse input when
+        `fit_intercept` is True.

         .. versionadded:: 0.17
            Stochastic Average Gradient descent solver.

diff --git a/sklearn/linear_model/tests/test_ridge.py b/sklearn/linear_model/tests/test_ridge.py
index 269952ddad659..e3bd6c6146ee9 100644
--- a/sklearn/linear_model/tests/test_ridge.py
+++ b/sklearn/linear_model/tests/test_ridge.py
@@ -1362,7 +1362,7 @@ def test_n_iter():
         assert reg.n_iter_ is None

-@pytest.mark.parametrize("solver", ["sparse_cg", "lbfgs", "auto"])
+@pytest.mark.parametrize("solver", ["lsqr", "sparse_cg", "lbfgs", "auto"])
 @pytest.mark.parametrize("with_sample_weight", [True, False])
 def test_ridge_fit_intercept_sparse(solver, with_sample_weight, global_random_seed):
     """Check that ridge finds the same coefs and intercept on dense and sparse input
@@ -1400,7 +1400,7 @@ def test_ridge_fit_intercept_sparse(solver, with_sample_weight, global_random_se
     assert_allclose(dense_ridge.coef_, sparse_ridge.coef_)

-@pytest.mark.parametrize("solver", ["saga", "lsqr", "svd", "cholesky"])
+@pytest.mark.parametrize("solver", ["saga", "svd", "cholesky"])
 def test_ridge_fit_intercept_sparse_error(solver):
     X, y = _make_sparse_offset_regression(n_features=20, random_state=0)
     X_csr = sp.csr_matrix(X)

sklearn/linear_model/tests/test_ridge.py::test_ridgecv_alphas_validation[params2-TypeError-alphas\\[2\\] must be an instance of float, not str-RidgeCV]

sklearn/linear_model/tests/test_ridge.py::test_ridge_params_validation[params0-ValueError-alpha == -1, must be >= 0.0]

sklearn/linear_model/tests/test_ridge.py::test_ridge_params_validation[params1-TypeError-alpha must be an instance of float, not str]

sklearn/linear_model/tests/test_ridge.py::test_ridgecv_alphas_validation[params0-ValueError-alphas\\[1\\] == -1, must be > 0.0-RidgeClassifierCV]

sklearn/linear_model/tests/test_ridge.py::test_ridge_params_validation[params5-TypeError-tol must be an instance of float, not str]

sklearn/linear_model/tests/test_ridge.py::test_ridgecv_alphas_validation[params2-TypeError-alphas\\[2\\] must be an instance of float, not str-RidgeClassifierCV]

sklearn/linear_model/tests/test_ridge.py::test_ridge_params_validation[params2-ValueError-max_iter == 0, must be >= 1.]

sklearn/linear_model/tests/test_ridge.py::test_ridgecv_alphas_validation[params0-ValueError-alphas\\[1\\] == -1, must be > 0.0-RidgeCV]

sklearn/linear_model/tests/test_ridge.py::test_ridgecv_alphas_validation[params1-ValueError-alphas\\[0\\] == -0.1, must be > 0.0-RidgeCV]

sklearn/linear_model/tests/test_ridge.py::test_ridgecv_alphas_validation[params1-ValueError-alphas\\[0\\] == -0.1, must be > 0.0-RidgeClassifierCV]

sklearn/linear_model/tests/test_ridge.py::test_ridge_params_validation[params3-TypeError-max_iter must be an instance of int, not str]

sklearn/linear_model/tests/test_ridge.py::test_ridge_params_validation[params4-ValueError-tol == -1.0, must be >= 0.]

diff --git a/doc/whats_new/v1.1.rst b/doc/whats_new/v1.1.rst
index fab4ec850..31b4ea007 100644
--- a/doc/whats_new/v1.1.rst
+++ b/doc/whats_new/v1.1.rst
@@ -636,6 +636,9 @@ Changelog
   of sample weights when the input is sparse.
   :pr:`22899` by :user:`Jérémie du Boisberranger <jeremiedbb>`.

+- |Feature| :class:`Ridge` with `solver="lsqr"` now supports to fit sparse input with
+  `fit_intercept=True`.
+
 :mod:`sklearn.manifold`
 .......................

If completing this task requires creating new files, classes, fields, or error messages, you may consider using the following suggested entity names:


diff --git a/doc/whats_new/v1.2.rst b/doc/whats_new/v1.2.rst
index 8e5851cca632f..fdc7685395c62 100644
--- a/doc/whats_new/v1.2.rst
+++ b/doc/whats_new/v1.2.rst
@@ -44,6 +44,13 @@ Changelog
 - |Enhancement| :class:`cluster.Birch` now preserves dtype for `numpy.float32`
   inputs. :pr:`22968` by `Meekail Zain <micky774>`.

+- |Enhancement| :class:`cluster.KMeans` and :class:`cluster.MiniBatchKMeans`
+  now accept a new `'auto'` option for `n_init` which changes the number of
+  random initializations to one when using `init='k-means++'` for efficiency.
+  This begins deprecation for the default values of `n_init` in the two classes
+  and both will have their defaults changed to `n_init='auto'` in 1.4.
+  :pr:`23038` by :user:`Meekail Zain <micky774>`.
+
 :mod:`sklearn.datasets`
 .......................

diff --git a/sklearn/cluster/_kmeans.py b/sklearn/cluster/_kmeans.py
index 2eaee8529e14b..2f960ed7593ef 100644
--- a/sklearn/cluster/_kmeans.py
+++ b/sklearn/cluster/_kmeans.py
@@ -272,7 +272,7 @@ def k_means(
     *,
     sample_weight=None,
     init="k-means++",
-    n_init=10,
+    n_init="warn",
     max_iter=300,
     verbose=False,
     tol=1e-4,
@@ -314,10 +314,19 @@ def k_means(
         - If a callable is passed, it should take arguments `X`, `n_clusters` and a
           random state and return an initialization.

-    n_init : int, default=10
+    n_init : 'auto' or int, default=10
         Number of time the k-means algorithm will be run with different
         centroid seeds. The final results will be the best output of
-        `n_init` consecutive runs in terms of inertia.
+        n_init consecutive runs in terms of inertia.
+
+        When `n_init='auto'`, the number of runs will be 10 if using
+        `init='random'`, and 1 if using `init='kmeans++'`.
+
+        .. versionadded:: 1.2
+           Added 'auto' option for `n_init`.
+
+        .. versionchanged:: 1.4
+           Default value for `n_init` will change from 10 to `'auto'` in version 1.4.

     max_iter : int, default=300
         Maximum number of iterations of the k-means algorithm to run.
@@ -803,7 +812,10 @@ class _BaseKMeans(
     _parameter_constraints = {
         "n_clusters": [Interval(Integral, 1, None, closed="left")],
         "init": [StrOptions({"k-means++", "random"}), callable, "array-like"],
-        "n_init": [Interval(Integral, 1, None, closed="left")],
+        "n_init": [
+            StrOptions({"auto", "warn"}),
+            Interval(Integral, 1, None, closed="left"),
+        ],
         "max_iter": [Interval(Integral, 1, None, closed="left")],
         "tol": [Interval(Real, 0, None, closed="left")],
         "verbose": [Interval(Integral, 0, None, closed="left"), bool],
@@ -829,7 +841,7 @@ def __init__(
         self.verbose = verbose
         self.random_state = random_state

-    def _check_params_vs_input(self, X):
+    def _check_params_vs_input(self, X, default_n_init=None):
         # n_clusters
         if X.shape[0] < self.n_clusters:
             raise ValueError(
@@ -839,8 +851,23 @@ def _check_params_vs_input(self, X):
         # tol
         self._tol = _tolerance(X, self.tol)

-        # init
+        # n-init
+        # TODO(1.4): Remove
         self._n_init = self.n_init
+        if self._n_init == "warn":
+            warnings.warn(
+                "The default value of `n_init` will change from "
+                f"{default_n_init} to 'auto' in 1.4. Set the value of `n_init`"
+                " explicitly to suppress the warning",
+                FutureWarning,
+            )
+            self._n_init = default_n_init
+        if self._n_init == "auto":
+            if self.init == "k-means++":
+                self._n_init = 1
+            else:
+                self._n_init = default_n_init
+
         if _is_arraylike_not_scalar(self.init) and self._n_init != 1:
             warnings.warn(
                 "Explicit initial center position passed: performing only"
@@ -1150,11 +1177,20 @@ class KMeans(_BaseKMeans):
         If a callable is passed, it should take arguments X, n_clusters and a
         random state and return an initialization.

-    n_init : int, default=10
+    n_init : 'auto' or int, default=10
         Number of time the k-means algorithm will be run with different
         centroid seeds. The final results will be the best output of
         n_init consecutive runs in terms of inertia.

+        When `n_init='auto'`, the number of runs will be 10 if using
+        `init='random'`, and 1 if using `init='kmeans++'`.
+
+        .. versionadded:: 1.2
+           Added 'auto' option for `n_init`.
+
+        .. versionchanged:: 1.4
+           Default value for `n_init` will change from 10 to `'auto'` in version 1.4.
+
     max_iter : int, default=300
         Maximum number of iterations of the k-means algorithm for a
         single run.
@@ -1263,7 +1299,7 @@ class KMeans(_BaseKMeans):
     >>> import numpy as np
     >>> X = np.array([[1, 2], [1, 4], [1, 0],
     ...               [10, 2], [10, 4], [10, 0]])
-    >>> kmeans = KMeans(n_clusters=2, random_state=0).fit(X)
+    >>> kmeans = KMeans(n_clusters=2, random_state=0, n_init="auto").fit(X)
     >>> kmeans.labels_
     array([1, 1, 1, 0, 0, 0], dtype=int32)
     >>> kmeans.predict([[0, 0], [12, 3]])
@@ -1286,7 +1322,7 @@ def __init__(
         n_clusters=8,
         *,
         init="k-means++",
-        n_init=10,
+        n_init="warn",
         max_iter=300,
         tol=1e-4,
         verbose=0,
@@ -1308,7 +1344,7 @@ def __init__(
         self.algorithm = algorithm

     def _check_params_vs_input(self, X):
-        super()._check_params_vs_input(X)
+        super()._check_params_vs_input(X, default_n_init=10)

         self._algorithm = self.algorithm
         if self._algorithm in ("auto", "full"):
@@ -1667,11 +1703,20 @@ class MiniBatchKMeans(_BaseKMeans):
         If `None`, the heuristic is `init_size = 3 * batch_size` if
         `3 * batch_size < n_clusters`, else `init_size = 3 * n_clusters`.

-    n_init : int, default=3
+    n_init : 'auto' or int, default=3
         Number of random initializations that are tried.
         In contrast to KMeans, the algorithm is only run once, using the
         best of the ``n_init`` initializations as measured by inertia.

+        When `n_init='auto'`, the number of runs will be 3 if using
+        `init='random'`, and 1 if using `init='kmeans++'`.
+
+        .. versionadded:: 1.2
+           Added 'auto' option for `n_init`.
+
+        .. versionchanged:: 1.4
+           Default value for `n_init` will change from 3 to `'auto'` in version 1.4.
+
     reassignment_ratio : float, default=0.01
         Control the fraction of the maximum number of counts for a center to
         be reassigned. A higher value means that low count centers are more
@@ -1737,7 +1782,8 @@ class MiniBatchKMeans(_BaseKMeans):
     >>> # manually fit on batches
     >>> kmeans = MiniBatchKMeans(n_clusters=2,
     ...                          random_state=0,
-    ...                          batch_size=6)
+    ...                          batch_size=6,
+    ...                          n_init="auto")
     >>> kmeans = kmeans.partial_fit(X[0:6,:])
     >>> kmeans = kmeans.partial_fit(X[6:12,:])
     >>> kmeans.cluster_centers_
@@ -1749,12 +1795,13 @@ class MiniBatchKMeans(_BaseKMeans):
     >>> kmeans = MiniBatchKMeans(n_clusters=2,
     ...                          random_state=0,
     ...                          batch_size=6,
-    ...                          max_iter=10).fit(X)
+    ...                          max_iter=10,
+    ...                          n_init="auto").fit(X)
     >>> kmeans.cluster_centers_
-    array([[1.19..., 1.22...],
-           [4.03..., 2.46...]])
+    array([[3.97727273, 2.43181818],
+           [1.125     , 1.6       ]])
     >>> kmeans.predict([[0, 0], [4, 4]])
-    array([0, 1], dtype=int32)
+    array([1, 0], dtype=int32)
     """

     _parameter_constraints = {
@@ -1779,7 +1826,7 @@ def __init__(
         tol=0.0,
         max_no_improvement=10,
         init_size=None,
-        n_init=3,
+        n_init="warn",
         reassignment_ratio=0.01,
     ):

@@ -1800,7 +1847,7 @@ def __init__(
         self.reassignment_ratio = reassignment_ratio

     def _check_params_vs_input(self, X):
-        super()._check_params_vs_input(X)
+        super()._check_params_vs_input(X, default_n_init=3)

         self._batch_size = min(self.batch_size, X.shape[0])

diff --git a/sklearn/cluster/tests/test_k_means.py b/sklearn/cluster/tests/test_k_means.py
index eabdb746e4dd5..9939852987775 100644
--- a/sklearn/cluster/tests/test_k_means.py
+++ b/sklearn/cluster/tests/test_k_means.py
@@ -1,6 +1,7 @@
 """Testing for K-means"""
 import re
 import sys
+import warnings

 import numpy as np
 from scipy import sparse as sp
@@ -31,6 +32,12 @@
 from sklearn.datasets import make_blobs
 from io import StringIO

+# TODO(1.4): Remove
+msg = (
+    r"The default value of `n_init` will change from \d* to 'auto' in 1.4. Set the"
+    r" value of `n_init` explicitly to suppress the warning:FutureWarning"
+)
+pytestmark = pytest.mark.filterwarnings("ignore:" + msg)

 # non centered, sparse centers to check the
 centers = np.array(
@@ -1029,6 +1036,35 @@ def test_inertia(dtype):
     assert_allclose(inertia_sparse, expected, rtol=1e-6)

+# TODO(1.4): Remove
+@pytest.mark.parametrize("Klass, default_n_init", [(KMeans, 10), (MiniBatchKMeans, 3)])
+def test_change_n_init_future_warning(Klass, default_n_init):
+    est = Klass(n_init=1)
+    with warnings.catch_warnings():
+        warnings.simplefilter("error", FutureWarning)
+        est.fit(X)
+
+    default_n_init = 10 if Klass.__name__ == "KMeans" else 3
+    msg = (
+        f"The default value of `n_init` will change from {default_n_init} to 'auto'"
+        " in 1.4"
+    )
+    est = Klass()
+    with pytest.warns(FutureWarning, match=msg):
+        est.fit(X)
+
+
+@pytest.mark.parametrize("Klass, default_n_init", [(KMeans, 10), (MiniBatchKMeans, 3)])
+def test_n_init_auto(Klass, default_n_init):
+    est = Klass(n_init="auto", init="k-means++")
+    est.fit(X)
+    assert est._n_init == 1
+
+    est = Klass(n_init="auto", init="random")
+    est.fit(X)
+    assert est._n_init == 10 if Klass.__name__ == "KMeans" else 3
+
+
 @pytest.mark.parametrize("Estimator", [KMeans, MiniBatchKMeans])
 def test_sample_weight_unchanged(Estimator):
     # Check that sample_weight is not modified in place by KMeans (#17204)
diff --git a/sklearn/inspection/tests/test_partial_dependence.py b/sklearn/inspection/tests/test_partial_dependence.py
index 4e62f140c6953..5fe5ddb5d6db1 100644
--- a/sklearn/inspection/tests/test_partial_dependence.py
+++ b/sklearn/inspection/tests/test_partial_dependence.py
@@ -427,7 +427,7 @@ def fit(self, X, y):
     "estimator, params, err_msg",
     [
         (
-            KMeans(),
+            KMeans(random_state=0, n_init="auto"),
             {"features": [0]},
             "'estimator' must be a fitted regressor or classifier",
         ),
diff --git a/sklearn/manifold/tests/test_spectral_embedding.py b/sklearn/manifold/tests/test_spectral_embedding.py
index 935e5408a4159..38e555643b034 100644
--- a/sklearn/manifold/tests/test_spectral_embedding.py
+++ b/sklearn/manifold/tests/test_spectral_embedding.py
@@ -329,7 +329,7 @@ def test_pipeline_spectral_clustering(seed=36):
         random_state=random_state,
     )
     for se in [se_rbf, se_knn]:
-        km = KMeans(n_clusters=n_clusters, random_state=random_state)
+        km = KMeans(n_clusters=n_clusters, random_state=random_state, n_init="auto")
         km.fit(se.fit_transform(S))
         assert_array_almost_equal(
             normalized_mutual_info_score(km.labels_, true_labels), 1.0, 2
diff --git a/sklearn/metrics/tests/test_score_objects.py b/sklearn/metrics/tests/test_score_objects.py
index 204a895742db7..b909e182624cf 100644
--- a/sklearn/metrics/tests/test_score_objects.py
+++ b/sklearn/metrics/tests/test_score_objects.py
@@ -577,7 +577,7 @@ def test_supervised_cluster_scorers():
     # Test clustering scorers against gold standard labeling.
     X, y = make_blobs(random_state=0, centers=2)
     X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
-    km = KMeans(n_clusters=3)
+    km = KMeans(n_clusters=3, n_init="auto")
     km.fit(X_train)
     for name in CLUSTER_SCORERS:
         score1 = get_scorer(name)(km, X_test, y_test)
diff --git a/sklearn/model_selection/tests/test_validation.py b/sklearn/model_selection/tests/test_validation.py
index 90b5a605ac2e4..8a3c5619c43e7 100644
--- a/sklearn/model_selection/tests/test_validation.py
+++ b/sklearn/model_selection/tests/test_validation.py
@@ -930,7 +930,7 @@ def test_cross_val_predict():
     preds = cross_val_predict(est, Xsp, y)
     assert_array_almost_equal(len(preds), len(y))

-    preds = cross_val_predict(KMeans(), X)
+    preds = cross_val_predict(KMeans(n_init="auto"), X)
     assert len(preds) == len(y)

     class BadCV:
diff --git a/sklearn/tests/test_discriminant_analysis.py b/sklearn/tests/test_discriminant_analysis.py
index 9ef444c67b3e1..9f0ac68899add 100644
--- a/sklearn/tests/test_discriminant_analysis.py
+++ b/sklearn/tests/test_discriminant_analysis.py
@@ -143,7 +143,7 @@ def test_lda_predict():

     # test bad covariance estimator
     clf = LinearDiscriminantAnalysis(
-        solver="lsqr", covariance_estimator=KMeans(n_clusters=2)
+        solver="lsqr", covariance_estimator=KMeans(n_clusters=2, n_init="auto")
     )
     with pytest.raises(
         ValueError, match="KMeans does not have a covariance_ attribute"
diff --git a/sklearn/tests/test_docstring_parameters.py b/sklearn/tests/test_docstring_parameters.py
index 5e22b425be1ec..4355af286ac67 100644
--- a/sklearn/tests/test_docstring_parameters.py
+++ b/sklearn/tests/test_docstring_parameters.py
@@ -262,6 +262,10 @@ def test_fit_docstring_attributes(name, Estimator):
     if Estimator.__name__ == "MiniBatchDictionaryLearning":
         est.set_params(batch_size=5)

+    # TODO(1.4): TO BE REMOVED for 1.4 (avoid FutureWarning)
+    if Estimator.__name__ in ("KMeans", "MiniBatchKMeans"):
+        est.set_params(n_init="auto")
+
     # In case we want to deprecate some attributes in the future
     skipped_attributes = {}

diff --git a/sklearn/tests/test_pipeline.py b/sklearn/tests/test_pipeline.py
index 6913815191ea8..0ce71bf9b55f4 100644
--- a/sklearn/tests/test_pipeline.py
+++ b/sklearn/tests/test_pipeline.py
@@ -422,11 +422,11 @@ def test_fit_predict_on_pipeline():
     # test that the fit_predict on pipeline yields same results as applying
     # transform and clustering steps separately
     scaler = StandardScaler()
-    km = KMeans(random_state=0)
+    km = KMeans(random_state=0, n_init="auto")
     # As pipeline doesn't clone estimators on construction,
     # it must have its own estimators
     scaler_for_pipeline = StandardScaler()
-    km_for_pipeline = KMeans(random_state=0)
+    km_for_pipeline = KMeans(random_state=0, n_init="auto")

     # first compute the transform and clustering step separately
     scaled = scaler.fit_transform(iris.data)

sklearn/model_selection/tests/test_validation.py::test_fit_and_score_verbosity[False-scorer2-10-split_prg2-cdt_prg2-\\[CV 2/3; 1/1\\] END ....... sc1: \\(test=3.421\\) sc2: \\(test=3.421\\) total time=   0.\\ds]

sklearn/model_selection/tests/test_validation.py::test_fit_and_score_verbosity[True-scorer1-3-split_prg1-cdt_prg1-\\[CV 2/3\\] END  sc1: \\(train=3.421, test=3.421\\) sc2: \\(train=3.421, test=3.421\\) total time=   0.\\ds]

sklearn/model_selection/tests/test_validation.py::test_fit_and_score_verbosity[False-three_params_scorer-2-split_prg0-cdt_prg0-\\[CV\\] END .................................................... total time=   0.\\ds]

diff --git a/doc/whats_new/v1.2.rst b/doc/whats_new/v1.2.rst
index 8e5851cca..6eed22c60 100644
--- a/doc/whats_new/v1.2.rst
+++ b/doc/whats_new/v1.2.rst
@@ -44,6 +44,12 @@ Changelog
 - |Enhancement| :class:`cluster.Birch` now preserves dtype for `numpy.float32`
   inputs. :pr:`22968` by `Meekail Zain <micky774>`.

+- |Enhancement| :class:`cluster.KMeans` and :class:`cluster.MiniBatchKMeans`
+  now accept a new `'auto'` option for `n_init` which changes the number of
+  random initializations to one when using `init='k-means++'` for efficiency.
+  This begins deprecation for the default values of `n_init` in the two classes
+  and both will have their defaults changed to `n_init='auto'` in 1.4.
+
 :mod:`sklearn.datasets`
 .......................


diff --git a/doc/whats_new/v1.1.rst b/doc/whats_new/v1.1.rst
index 61d5c64255f71..eb303997f577b 100644
--- a/doc/whats_new/v1.1.rst
+++ b/doc/whats_new/v1.1.rst
@@ -177,6 +177,10 @@ Changelog
   preserve dtype for `numpy.float32`.
   :pr:`22111` by :user:`Takeshi Oura <takoika>`.

+- |Enhancement| :class:`decomposition.TruncatedSVD` now allows n_components == n_features, if
+  `algorithm='randomized'`.
+  :pr:`22181` by :user:`Zach Deane-Mayer <zachmayer>`.
+
 - |API| Adds :term:`get_feature_names_out` to all transformers in the
   :mod:`~sklearn.decomposition` module:
   :class:`~sklearn.decomposition.DictionaryLearning`,
diff --git a/sklearn/decomposition/_truncated_svd.py b/sklearn/decomposition/_truncated_svd.py
index 01d79f742302f..d6791114171bf 100644
--- a/sklearn/decomposition/_truncated_svd.py
+++ b/sklearn/decomposition/_truncated_svd.py
@@ -6,6 +6,7 @@
 #         Michael Becker <mike@beckerfuffle.com>
 # License: 3-clause BSD.

+from numbers import Integral
 import numpy as np
 import scipy.sparse as sp
 from scipy.sparse.linalg import svds
@@ -15,8 +16,7 @@
 from ..utils._arpack import _init_arpack_v0
 from ..utils.extmath import randomized_svd, safe_sparse_dot, svd_flip
 from ..utils.sparsefuncs import mean_variance_axis
-from ..utils.validation import check_is_fitted
-
+from ..utils.validation import check_is_fitted, check_scalar

 __all__ = ["TruncatedSVD"]

@@ -44,7 +44,8 @@ class TruncatedSVD(_ClassNamePrefixFeaturesOutMixin, TransformerMixin, BaseEstim
     ----------
     n_components : int, default=2
         Desired dimensionality of output data.
-        Must be strictly less than the number of features.
+        If algorithm='arpack', must be strictly less than the number of features.
+        If algorithm='randomized', must be less than or equal to the number of features.
         The default value is useful for visualisation. For LSA, a value of
         100 is recommended.

@@ -203,10 +204,13 @@ def fit_transform(self, X, y=None):
         elif self.algorithm == "randomized":
             k = self.n_components
             n_features = X.shape[1]
-            if k >= n_features:
-                raise ValueError(
-                    "n_components must be < n_features; got %d >= %d" % (k, n_features)
-                )
+            check_scalar(
+                k,
+                "n_components",
+                target_type=Integral,
+                min_val=1,
+                max_val=n_features,
+            )
             U, Sigma, VT = randomized_svd(
                 X, self.n_components, n_iter=self.n_iter, random_state=random_state
             )

diff --git a/sklearn/decomposition/tests/test_truncated_svd.py b/sklearn/decomposition/tests/test_truncated_svd.py
index f227585f4ccf7..eb4a0066ebfb4 100644
--- a/sklearn/decomposition/tests/test_truncated_svd.py
+++ b/sklearn/decomposition/tests/test_truncated_svd.py
@@ -39,7 +39,7 @@ def test_solvers(X_sparse, solver, kind):
     assert_allclose(comp_a[9:], comp[9:], atol=1e-2)

-@pytest.mark.parametrize("n_components", (10, 25, 41))
+@pytest.mark.parametrize("n_components", (10, 25, 41, 55))
 def test_attributes(n_components, X_sparse):
     n_features = X_sparse.shape[1]
     tsvd = TruncatedSVD(n_components).fit(X_sparse)
@@ -47,13 +47,18 @@ def test_attributes(n_components, X_sparse):
     assert tsvd.components_.shape == (n_components, n_features)

-@pytest.mark.parametrize("algorithm", SVD_SOLVERS)
-def test_too_many_components(algorithm, X_sparse):
-    n_features = X_sparse.shape[1]
-    for n_components in (n_features, n_features + 1):
-        tsvd = TruncatedSVD(n_components=n_components, algorithm=algorithm)
-        with pytest.raises(ValueError):
-            tsvd.fit(X_sparse)
+@pytest.mark.parametrize(
+    "algorithm, n_components",
+    [
+        ("arpack", 55),
+        ("arpack", 56),
+        ("randomized", 56),
+    ],
+)
+def test_too_many_components(X_sparse, algorithm, n_components):
+    tsvd = TruncatedSVD(n_components=n_components, algorithm=algorithm)
+    with pytest.raises(ValueError):
+        tsvd.fit(X_sparse)

 @pytest.mark.parametrize("fmt", ("array", "csr", "csc", "coo", "lil"))

diff --git a/doc/whats_new/v1.1.rst b/doc/whats_new/v1.1.rst
index 61d5c6425..eac30a0b9 100644
--- a/doc/whats_new/v1.1.rst
+++ b/doc/whats_new/v1.1.rst
@@ -177,6 +177,9 @@ Changelog
   preserve dtype for `numpy.float32`.
   :pr:`22111` by :user:`Takeshi Oura <takoika>`.

+- |Enhancement| :class:`decomposition.TruncatedSVD` now allows n_components == n_features, if
+  `algorithm='randomized'`.
+
 - |API| Adds :term:`get_feature_names_out` to all transformers in the
   :mod:`~sklearn.decomposition` module:
   :class:`~sklearn.decomposition.DictionaryLearning`,


diff --git a/doc/whats_new/v1.2.rst b/doc/whats_new/v1.2.rst
index 2294d431909b0..49e589157851f 100644
--- a/doc/whats_new/v1.2.rst
+++ b/doc/whats_new/v1.2.rst
@@ -397,6 +397,13 @@ Changelog
   :class:`neighbors.RadiusNeighborsTransformer`.
   :pr:`24075` by :user:`Valentin Laurent <Valentin-Laurent>`.

+:mod:`sklearn.pipeline`
+.......................
+
+- |Enhancement| :meth:`pipeline.FeatureUnion.get_feature_names_out` can now
+  be used when one of the transformers in the :class:`pipeline.FeatureUnion` is
+  `"passthrough"`. :pr:`24058` by :user:`Diederik Perdok <diederikwp>`
+
 :mod:`sklearn.preprocessing`
 ............................

diff --git a/sklearn/pipeline.py b/sklearn/pipeline.py
index 086bc4e1f0557..0240865571ba8 100644
--- a/sklearn/pipeline.py
+++ b/sklearn/pipeline.py
@@ -1053,7 +1053,7 @@ def _iter(self):
             if trans == "drop":
                 continue
             if trans == "passthrough":
-                trans = FunctionTransformer()
+                trans = FunctionTransformer(feature_names_out="one-to-one")
             yield (name, trans, get_weight(name))

     @deprecated(

diff --git a/sklearn/tests/test_pipeline.py b/sklearn/tests/test_pipeline.py
index 0ce71bf9b55f4..a20b68a7ec905 100644
--- a/sklearn/tests/test_pipeline.py
+++ b/sklearn/tests/test_pipeline.py
@@ -1013,29 +1013,42 @@ def test_set_feature_union_passthrough():
     """Check the behaviour of setting a transformer to `"passthrough"`."""
     mult2 = Mult(2)
     mult3 = Mult(3)
+
+    # We only test get_features_names_out, as get_feature_names is unsupported by
+    # FunctionTransformer, and hence unsupported by FeatureUnion passthrough.
+    mult2.get_feature_names_out = lambda input_features: ["x2"]
+    mult3.get_feature_names_out = lambda input_features: ["x3"]
+
     X = np.asarray([[1]])

     ft = FeatureUnion([("m2", mult2), ("m3", mult3)])
     assert_array_equal([[2, 3]], ft.fit(X).transform(X))
     assert_array_equal([[2, 3]], ft.fit_transform(X))
+    assert_array_equal(["m2__x2", "m3__x3"], ft.get_feature_names_out())

     ft.set_params(m2="passthrough")
     assert_array_equal([[1, 3]], ft.fit(X).transform(X))
     assert_array_equal([[1, 3]], ft.fit_transform(X))
+    assert_array_equal(["m2__myfeat", "m3__x3"], ft.get_feature_names_out(["myfeat"]))

     ft.set_params(m3="passthrough")
     assert_array_equal([[1, 1]], ft.fit(X).transform(X))
     assert_array_equal([[1, 1]], ft.fit_transform(X))
+    assert_array_equal(
+        ["m2__myfeat", "m3__myfeat"], ft.get_feature_names_out(["myfeat"])
+    )

     # check we can change back
     ft.set_params(m3=mult3)
     assert_array_equal([[1, 3]], ft.fit(X).transform(X))
     assert_array_equal([[1, 3]], ft.fit_transform(X))
+    assert_array_equal(["m2__myfeat", "m3__x3"], ft.get_feature_names_out(["myfeat"]))

     # Check 'passthrough' step at construction time
     ft = FeatureUnion([("m2", "passthrough"), ("m3", mult3)])
     assert_array_equal([[1, 3]], ft.fit(X).transform(X))
     assert_array_equal([[1, 3]], ft.fit_transform(X))
+    assert_array_equal(["m2__myfeat", "m3__x3"], ft.get_feature_names_out(["myfeat"]))

     X = iris.data
     columns = X.shape[1]
@@ -1044,16 +1057,51 @@ def test_set_feature_union_passthrough():
     ft = FeatureUnion([("passthrough", "passthrough"), ("pca", pca)])
     assert_array_equal(X, ft.fit(X).transform(X)[:, :columns])
     assert_array_equal(X, ft.fit_transform(X)[:, :columns])
+    assert_array_equal(
+        [
+            "passthrough__f0",
+            "passthrough__f1",
+            "passthrough__f2",
+            "passthrough__f3",
+            "pca__pca0",
+            "pca__pca1",
+        ],
+        ft.get_feature_names_out(["f0", "f1", "f2", "f3"]),
+    )

     ft.set_params(pca="passthrough")
     X_ft = ft.fit(X).transform(X)
     assert_array_equal(X_ft, np.hstack([X, X]))
     X_ft = ft.fit_transform(X)
     assert_array_equal(X_ft, np.hstack([X, X]))
+    assert_array_equal(
+        [
+            "passthrough__f0",
+            "passthrough__f1",
+            "passthrough__f2",
+            "passthrough__f3",
+            "pca__f0",
+            "pca__f1",
+            "pca__f2",
+            "pca__f3",
+        ],
+        ft.get_feature_names_out(["f0", "f1", "f2", "f3"]),
+    )

     ft.set_params(passthrough=pca)
     assert_array_equal(X, ft.fit(X).transform(X)[:, -columns:])
     assert_array_equal(X, ft.fit_transform(X)[:, -columns:])
+    assert_array_equal(
+        [
+            "passthrough__pca0",
+            "passthrough__pca1",
+            "pca__f0",
+            "pca__f1",
+            "pca__f2",
+            "pca__f3",
+        ],
+        ft.get_feature_names_out(["f0", "f1", "f2", "f3"]),
+    )

     ft = FeatureUnion(
         [("passthrough", "passthrough"), ("pca", pca)],
@@ -1061,6 +1109,39 @@ def test_set_feature_union_passthrough():
     )
     assert_array_equal(X * 2, ft.fit(X).transform(X)[:, :columns])
     assert_array_equal(X * 2, ft.fit_transform(X)[:, :columns])
+    assert_array_equal(
+        [
+            "passthrough__f0",
+            "passthrough__f1",
+            "passthrough__f2",
+            "passthrough__f3",
+            "pca__pca0",
+            "pca__pca1",
+        ],
+        ft.get_feature_names_out(["f0", "f1", "f2", "f3"]),
+    )
+
+
+def test_feature_union_passthrough_get_feature_names_out():
+    """Check that get_feature_names_out works with passthrough without
+    passing input_features.
+    """
+    X = iris.data
+    pca = PCA(n_components=2, svd_solver="randomized", random_state=0)
+
+    ft = FeatureUnion([("pca", pca), ("passthrough", "passthrough")])
+    ft.fit(X)
+    assert_array_equal(
+        [
+            "pca__pca0",
+            "pca__pca1",
+            "passthrough__x0",
+            "passthrough__x1",
+            "passthrough__x2",
+            "passthrough__x3",
+        ],
+        ft.get_feature_names_out(),
+    )

 def test_step_name_validation():

sklearn/tests/test_pipeline.py::test_verbose[est6-\\[Pipeline\\].*\\(step 1 of 2\\) Processing transf.* total=.*\\n\\[Pipeline\\].*\\(step 2 of 2\\) Processing clf.* total=.*\\n$-fit]

sklearn/tests/test_pipeline.py::test_verbose[est15-\\[FeatureUnion\\].*\\(step 1 of 1\\) Processing mult2.* total=.*\\n$-fit_transform]

sklearn/tests/test_pipeline.py::test_verbose[est11-\\[Pipeline\\].*\\(step 1 of 2\\) Processing transf.* total=.*\\n\\[Pipeline\\].*\\(step 2 of 2\\) Processing mult.* total=.*\\n$-fit_transform]

sklearn/tests/test_pipeline.py::test_verbose[est13-\\[FeatureUnion\\].*\\(step 1 of 2\\) Processing mult1.* total=.*\\n\\[FeatureUnion\\].*\\(step 2 of 2\\) Processing mult2.* total=.*\\n$-fit_transform]

sklearn/tests/test_pipeline.py::test_verbose[est9-\\[Pipeline\\].*\\(step 1 of 2\\) Processing transf.* total=.*\\n\\[Pipeline\\].*\\(step 2 of 2\\) Processing mult.* total=.*\\n$-fit_transform]

sklearn/tests/test_pipeline.py::test_verbose[est5-\\[Pipeline\\].*\\(step 1 of 3\\) Processing transf.* total=.*\\n\\[Pipeline\\].*\\(step 2 of 3\\) Processing noop.* total=.*\\n\\[Pipeline\\].*\\(step 3 of 3\\) Processing clf.* total=.*\\n$-fit_predict]

sklearn/tests/test_pipeline.py::test_verbose[est4-\\[Pipeline\\].*\\(step 1 of 3\\) Processing transf.* total=.*\\n\\[Pipeline\\].*\\(step 2 of 3\\) Processing noop.* total=.*\\n\\[Pipeline\\].*\\(step 3 of 3\\) Processing clf.* total=.*\\n$-fit]

sklearn/tests/test_pipeline.py::test_verbose[est14-\\[FeatureUnion\\].*\\(step 1 of 1\\) Processing mult2.* total=.*\\n$-fit]

sklearn/tests/test_pipeline.py::test_verbose[est3-\\[Pipeline\\].*\\(step 1 of 3\\) Processing transf.* total=.*\\n\\[Pipeline\\].*\\(step 2 of 3\\) Processing noop.* total=.*\\n\\[Pipeline\\].*\\(step 3 of 3\\) Processing clf.* total=.*\\n$-fit_predict]

sklearn/tests/test_pipeline.py::test_verbose[est0-\\[Pipeline\\].*\\(step 1 of 2\\) Processing transf.* total=.*\\n\\[Pipeline\\].*\\(step 2 of 2\\) Processing clf.* total=.*\\n$-fit]

sklearn/tests/test_pipeline.py::test_verbose[est12-\\[FeatureUnion\\].*\\(step 1 of 2\\) Processing mult1.* total=.*\\n\\[FeatureUnion\\].*\\(step 2 of 2\\) Processing mult2.* total=.*\\n$-fit]

sklearn/tests/test_pipeline.py::test_verbose[est2-\\[Pipeline\\].*\\(step 1 of 3\\) Processing transf.* total=.*\\n\\[Pipeline\\].*\\(step 2 of 3\\) Processing noop.* total=.*\\n\\[Pipeline\\].*\\(step 3 of 3\\) Processing clf.* total=.*\\n$-fit]

sklearn/tests/test_pipeline.py::test_verbose[est7-\\[Pipeline\\].*\\(step 1 of 2\\) Processing transf.* total=.*\\n\\[Pipeline\\].*\\(step 2 of 2\\) Processing clf.* total=.*\\n$-fit_transform]

sklearn/tests/test_pipeline.py::test_verbose[est8-\\[Pipeline\\].*\\(step 1 of 2\\) Processing transf.* total=.*\\n\\[Pipeline\\].*\\(step 2 of 2\\) Processing mult.* total=.*\\n$-fit]

sklearn/tests/test_pipeline.py::test_verbose[est10-\\[Pipeline\\].*\\(step 1 of 2\\) Processing transf.* total=.*\\n\\[Pipeline\\].*\\(step 2 of 2\\) Processing mult.* total=.*\\n$-fit]

sklearn/tests/test_pipeline.py::test_verbose[est1-\\[Pipeline\\].*\\(step 1 of 2\\) Processing transf.* total=.*\\n\\[Pipeline\\].*\\(step 2 of 2\\) Processing clf.* total=.*\\n$-fit_predict]

diff --git a/doc/whats_new/v1.2.rst b/doc/whats_new/v1.2.rst
index 2294d4319..1806d2062 100644
--- a/doc/whats_new/v1.2.rst
+++ b/doc/whats_new/v1.2.rst
@@ -397,6 +397,13 @@ Changelog
   :class:`neighbors.RadiusNeighborsTransformer`.
   :pr:`24075` by :user:`Valentin Laurent <Valentin-Laurent>`.

+:mod:`sklearn.pipeline`
+.......................
+
+- |Enhancement| :meth:`pipeline.FeatureUnion.get_feature_names_out` can now
+  be used when one of the transformers in the :class:`pipeline.FeatureUnion` is
+  `"passthrough"`.
+
 :mod:`sklearn.preprocessing`
 ............................


diff --git a/doc/whats_new/v1.2.rst b/doc/whats_new/v1.2.rst
index 7b7c33cb71cfd..bbd2b5fedb44b 100644
--- a/doc/whats_new/v1.2.rst
+++ b/doc/whats_new/v1.2.rst
@@ -514,6 +514,9 @@ Changelog
   is now deprecated and will be removed in version 1.4. Use `sparse_output` instead.
   :pr:`24412` by :user:`Rushil Desai <rusdes>`.

+- |Fix| :class:`preprocessing.LabelEncoder` correctly encodes NaNs in `transform`.
+  :pr:`22629` by `Thomas Fan`_.
+
 :mod:`sklearn.svm`
 ..................

@@ -535,6 +538,9 @@ Changelog
   deterministic SVD used by the randomized SVD algorithm.
   :pr:`20617` by :user:`Srinath Kailasa <skailasa>`

+- |Enhancement| :func:`utils.validation.column_or_1d` now accepts a `dtype`
+  parameter to specific `y`'s dtype. :pr:`22629` by `Thomas Fan`_.
+
 - |FIX| :func:`utils.multiclass.type_of_target` now properly handles sparse matrices.
   :pr:`14862` by :user:`Léonard Binet <leonardbinet>`.

diff --git a/sklearn/preprocessing/_label.py b/sklearn/preprocessing/_label.py
index 5c1602cebbb05..b9da2254ad60f 100644
--- a/sklearn/preprocessing/_label.py
+++ b/sklearn/preprocessing/_label.py
@@ -131,7 +131,7 @@ def transform(self, y):
             Labels as normalized encodings.
         """
         check_is_fitted(self)
-        y = column_or_1d(y, warn=True)
+        y = column_or_1d(y, dtype=self.classes_.dtype, warn=True)
         # transform of empty array is empty array
         if _num_samples(y) == 0:
             return np.array([])
diff --git a/sklearn/utils/validation.py b/sklearn/utils/validation.py
index 021e640d1dd79..d204b332eef8c 100644
--- a/sklearn/utils/validation.py
+++ b/sklearn/utils/validation.py
@@ -1140,7 +1140,7 @@ def _check_y(y, multi_output=False, y_numeric=False, estimator=None):
     return y

-def column_or_1d(y, *, warn=False):
+def column_or_1d(y, *, dtype=None, warn=False):
     """Ravel column or 1d numpy array, else raises an error.

     Parameters
@@ -1148,6 +1148,11 @@ def column_or_1d(y, *, warn=False):
     y : array-like
        Input data.

+    dtype : data-type, default=None
+        Data type for `y`.
+
+        .. versionadded:: 1.2
+
     warn : bool, default=False
        To control display of warnings.

@@ -1162,7 +1167,7 @@ def column_or_1d(y, *, warn=False):
         If `y` is not a 1D array or a 2D array with a single row or column.
     """
     xp, _ = get_namespace(y)
-    y = xp.asarray(y)
+    y = xp.asarray(y, dtype=dtype)
     shape = y.shape
     if len(shape) == 1:
         return _asarray_with_order(xp.reshape(y, -1), order="C", xp=xp)

diff --git a/sklearn/preprocessing/tests/test_label.py b/sklearn/preprocessing/tests/test_label.py
index a59cd9b152d27..bd5ba6d2da9ee 100644
--- a/sklearn/preprocessing/tests/test_label.py
+++ b/sklearn/preprocessing/tests/test_label.py
@@ -643,3 +643,15 @@ def test_inverse_binarize_multiclass():
         csr_matrix([[0, 1, 0], [-1, 0, -1], [0, 0, 0]]), np.arange(3)
     )
     assert_array_equal(got, np.array([1, 1, 0]))
+
+
+def test_nan_label_encoder():
+    """Check that label encoder encodes nans in transform.
+
+    Non-regression test for #22628.
+    """
+    le = LabelEncoder()
+    le.fit(["a", "a", "b", np.nan])
+
+    y_trans = le.transform([np.nan])
+    assert_array_equal(y_trans, [2])

diff --git a/doc/whats_new/v1.2.rst b/doc/whats_new/v1.2.rst
index 7b7c33cb7..09263c83b 100644
--- a/doc/whats_new/v1.2.rst
+++ b/doc/whats_new/v1.2.rst
@@ -514,6 +514,8 @@ Changelog
   is now deprecated and will be removed in version 1.4. Use `sparse_output` instead.
   :pr:`24412` by :user:`Rushil Desai <rusdes>`.

+- |Fix| :class:`preprocessing.LabelEncoder` correctly encodes NaNs in `transform`.
+
 :mod:`sklearn.svm`
 ..................

@@ -535,6 +537,9 @@ Changelog
   deterministic SVD used by the randomized SVD algorithm.
   :pr:`20617` by :user:`Srinath Kailasa <skailasa>`

+- |Enhancement| :func:`utils.validation.column_or_1d` now accepts a `dtype`
+  parameter to specific `y`'s dtype.
+
 - |FIX| :func:`utils.multiclass.type_of_target` now properly handles sparse matrices.
   :pr:`14862` by :user:`Léonard Binet <leonardbinet>`.


diff --git a/doc/whats_new/v1.3.rst b/doc/whats_new/v1.3.rst
index 38bf60ab4cd64..a4d8de42c514b 100644
--- a/doc/whats_new/v1.3.rst
+++ b/doc/whats_new/v1.3.rst
@@ -238,6 +238,11 @@ Changelog
 - |Fix| :func:`datasets.fetch_openml` returns improved data types when
   `as_frame=True` and `parser="liac-arff"`. :pr:`26386` by `Thomas Fan`_.

+- |Enhancement| Allows to overwrite the parameters used to open the ARFF file using
+  the parameter `read_csv_kwargs` in :func:`datasets.fetch_openml` when using the
+  pandas parser.
+  :pr:`26433` by :user:`Guillaume Lemaitre <glemaitre>`.
+
 :mod:`sklearn.decomposition`
 ............................

diff --git a/sklearn/datasets/_openml.py b/sklearn/datasets/_openml.py
index 28713ca739ffd..d1a1285eb5a37 100644
--- a/sklearn/datasets/_openml.py
+++ b/sklearn/datasets/_openml.py
@@ -428,6 +428,7 @@ def _load_arff_response(
     md5_checksum: str,
     n_retries: int = 3,
     delay: float = 1.0,
+    read_csv_kwargs: Optional[Dict] = None,
 ):
     """Load the ARFF data associated with the OpenML URL.

@@ -470,6 +471,18 @@ def _load_arff_response(
     md5_checksum : str
         The MD5 checksum provided by OpenML to check the data integrity.

+    n_retries : int, default=3
+        The number of times to retry downloading the data if it fails.
+
+    delay : float, default=1.0
+        The delay between two consecutive downloads in seconds.
+
+    read_csv_kwargs : dict, default=None
+        Keyword arguments to pass to `pandas.read_csv` when using the pandas parser.
+        It allows to overwrite the default options.
+
+        .. versionadded:: 1.3
+
     Returns
     -------
     X : {ndarray, sparse matrix, dataframe}
@@ -506,13 +519,14 @@ def _open_url_and_load_gzip_file(url, data_home, n_retries, delay, arff_params):
         with closing(gzip_file):
             return load_arff_from_gzip_file(gzip_file, **arff_params)

-    arff_params = dict(
+    arff_params: Dict = dict(
         parser=parser,
         output_type=output_type,
         openml_columns_info=openml_columns_info,
         feature_names_to_select=feature_names_to_select,
         target_names_to_select=target_names_to_select,
         shape=shape,
+        read_csv_kwargs=read_csv_kwargs or {},
     )
     try:
         X, y, frame, categories = _open_url_and_load_gzip_file(
@@ -530,7 +544,7 @@ def _open_url_and_load_gzip_file(url, data_home, n_retries, delay, arff_params):
         # A parsing error could come from providing the wrong quotechar
         # to pandas. By default, we use a double quote. Thus, we retry
         # with a single quote before to raise the error.
-        arff_params["read_csv_kwargs"] = {"quotechar": "'"}
+        arff_params["read_csv_kwargs"].update(quotechar="'")
         X, y, frame, categories = _open_url_and_load_gzip_file(
             url, data_home, n_retries, delay, arff_params
         )
@@ -552,6 +566,7 @@ def _download_data_to_bunch(
     n_retries: int = 3,
     delay: float = 1.0,
     parser: str,
+    read_csv_kwargs: Optional[Dict] = None,
 ):
     """Download ARFF data, load it to a specific container and create to Bunch.

@@ -598,6 +613,12 @@ def _download_data_to_bunch(
     parser : {"liac-arff", "pandas"}
         The parser used to parse the ARFF file.

+    read_csv_kwargs : dict, default=None
+        Keyword arguments to pass to `pandas.read_csv` when using the pandas parser.
+        It allows to overwrite the default options.
+
+        .. versionadded:: 1.3
+
     Returns
     -------
     data : :class:`~sklearn.utils.Bunch`
@@ -657,6 +678,7 @@ def _download_data_to_bunch(
         md5_checksum=md5_checksum,
         n_retries=n_retries,
         delay=delay,
+        read_csv_kwargs=read_csv_kwargs,
     )

     return Bunch(
@@ -725,6 +747,7 @@ def fetch_openml(
     n_retries: int = 3,
     delay: float = 1.0,
     parser: Optional[str] = "warn",
+    read_csv_kwargs: Optional[Dict] = None,
 ):
     """Fetch dataset from openml by name or dataset id.

@@ -829,6 +852,13 @@ def fetch_openml(
            warning. Therefore, an `ImportError` will be raised from 1.4 if
            the dataset is dense and pandas is not installed.

+    read_csv_kwargs : dict, default=None
+        Keyword arguments passed to :func:`pandas.read_csv` when loading the data
+        from a ARFF file and using the pandas parser. It can allows to
+        overwrite some default parameters.
+
+        .. versionadded:: 1.3
+
     Returns
     -------
     data : :class:`~sklearn.utils.Bunch`
@@ -1096,6 +1126,7 @@ def fetch_openml(
         n_retries=n_retries,
         delay=delay,
         parser=parser_,
+        read_csv_kwargs=read_csv_kwargs,
     )

     if return_X_y:

diff --git a/sklearn/datasets/tests/test_openml.py b/sklearn/datasets/tests/test_openml.py
index fef03a5fd4d40..6f2ba36ff808a 100644
--- a/sklearn/datasets/tests/test_openml.py
+++ b/sklearn/datasets/tests/test_openml.py
@@ -1354,6 +1354,34 @@ def test_dataset_with_openml_warning(monkeypatch, gzip_response):
         fetch_openml(data_id=data_id, cache=False, as_frame=False, parser="liac-arff")

+def test_fetch_openml_overwrite_default_params_read_csv(monkeypatch):
+    """Check that we can overwrite the default parameters of `read_csv`."""
+    pytest.importorskip("pandas")
+    data_id = 1590
+    _monkey_patch_webbased_functions(monkeypatch, data_id=data_id, gzip_response=False)
+
+    common_params = {
+        "data_id": data_id,
+        "as_frame": True,
+        "cache": False,
+        "parser": "pandas",
+    }
+
+    # By default, the initial spaces are skipped. We checked that setting the parameter
+    # `skipinitialspace` to False will have an effect.
+    adult_without_spaces = fetch_openml(**common_params)
+    adult_with_spaces = fetch_openml(
+        **common_params, read_csv_kwargs={"skipinitialspace": False}
+    )
+    assert all(
+        cat.startswith(" ") for cat in adult_with_spaces.frame["class"].cat.categories
+    )
+    assert not any(
+        cat.startswith(" ")
+        for cat in adult_without_spaces.frame["class"].cat.categories
+    )
+
+
 ###############################################################################
 # Test cache, retry mechanisms, checksum, etc.

sklearn/datasets/tests/test_openml.py::test_fetch_openml_error[pandas-61-params1-ValueError-Can only handle homogeneous multi-target datasets-True]

sklearn/datasets/tests/test_openml.py::test_fetch_openml_error[liac-arff-40945-params2-ValueError-STRING attributes are not supported for array representation. Try as_frame=True-False]

sklearn/datasets/tests/test_openml.py::test_fetch_openml_raises_illegal_argument[params3-ValueError-Neither name nor data_id are provided. Please provide name or data_id.]

sklearn/datasets/tests/test_openml.py::test_fetch_openml_error[liac-arff-40675-params0-ValueError-No active dataset glass2 found-True]

sklearn/datasets/tests/test_openml.py::test_fetch_openml_error[liac-arff-61-params1-ValueError-Can only handle homogeneous multi-target datasets-False]

sklearn/datasets/tests/test_openml.py::test_fetch_openml_error[pandas-40675-params0-ValueError-No active dataset glass2 found-False]

sklearn/datasets/tests/test_openml.py::test_fetch_openml_error[pandas-40675-params0-ValueError-No active dataset glass2 found-True]

sklearn/datasets/tests/test_openml.py::test_fetch_openml_error[pandas-40945-params2-ValueError-STRING attributes are not supported for array representation. Try as_frame=True-False]

sklearn/datasets/tests/test_openml.py::test_fetch_openml_validation_parameter[params1-`as_frame` must be one of]

sklearn/datasets/tests/test_openml.py::test_fetch_openml_validation_parameter[params0-`parser` must be one of]

sklearn/datasets/tests/test_openml.py::test_fetch_openml_raises_illegal_argument[params2-ValueError-Dataset data_id=-1 and name=name passed, but you can only]

sklearn/datasets/tests/test_openml.py::test_fetch_openml_error[pandas-40945-params2-ValueError-STRING attributes are not supported for array representation. Try as_frame=True-True]

sklearn/datasets/tests/test_openml.py::test_fetch_openml_raises_illegal_argument[params1-ValueError-Dataset data_id=-1 and name=name passed, but you can only]

sklearn/datasets/tests/test_openml.py::test_fetch_openml_error[liac-arff-61-params1-ValueError-Can only handle homogeneous multi-target datasets-True]

sklearn/datasets/tests/test_openml.py::test_fetch_openml_sparse_arff_error[params0-Sparse ARFF datasets cannot be loaded with parser='pandas']

sklearn/datasets/tests/test_openml.py::test_fetch_openml_error[pandas-61-params1-ValueError-Can only handle homogeneous multi-target datasets-False]

sklearn/datasets/tests/test_openml.py::test_fetch_openml_error[liac-arff-40945-params2-ValueError-STRING attributes are not supported for array representation. Try as_frame=True-True]

sklearn/datasets/tests/test_openml.py::test_fetch_openml_raises_illegal_argument[params0-ValueError-Dataset data_id=-1 and version=version passed, but you can only]

sklearn/datasets/tests/test_openml.py::test_fetch_openml_sparse_arff_error[params1-Sparse ARFF datasets cannot be loaded with as_frame=True.]

sklearn/datasets/tests/test_openml.py::test_fetch_openml_error[liac-arff-40675-params0-ValueError-No active dataset glass2 found-False]

sklearn/datasets/tests/test_openml.py::test_fetch_openml_sparse_arff_error[params2-Sparse ARFF datasets cannot be loaded with as_frame=True.]

diff --git a/doc/whats_new/v1.3.rst b/doc/whats_new/v1.3.rst
index 38bf60ab4..79b380a28 100644
--- a/doc/whats_new/v1.3.rst
+++ b/doc/whats_new/v1.3.rst
@@ -238,6 +238,10 @@ Changelog
 - |Fix| :func:`datasets.fetch_openml` returns improved data types when
   `as_frame=True` and `parser="liac-arff"`. :pr:`26386` by `Thomas Fan`_.

+- |Enhancement| Allows to overwrite the parameters used to open the ARFF file using
+  the parameter `read_csv_kwargs` in :func:`datasets.fetch_openml` when using the
+  pandas parser.
+
 :mod:`sklearn.decomposition`
 ............................


diff --git a/doc/developers/develop.rst b/doc/developers/develop.rst
index 3476e00d98fd5..38531f66f9447 100644
--- a/doc/developers/develop.rst
+++ b/doc/developers/develop.rst
@@ -425,6 +425,31 @@ Objects that do not provide this method will be deep-copied
 (using the Python standard function ``copy.deepcopy``)
 if ``safe=False`` is passed to ``clone``.

+Estimators can customize the behavior of :func:`base.clone` by defining a
+`__sklearn_clone__` method. `__sklearn_clone__` must return an instance of the
+estimator. `__sklearn_clone__` is useful when an estimator needs to hold on to
+some state when :func:`base.clone` is called on the estimator. For example, a
+frozen meta-estimator for transformers can be defined as follows::
+
+    class FrozenTransformer(BaseEstimator):
+        def __init__(self, fitted_transformer):
+            self.fitted_transformer = fitted_transformer
+
+        def __getattr__(self, name):
+            # `fitted_transformer`'s attributes are now accessible
+            return getattr(self.fitted_transformer, name)
+
+        def __sklearn_clone__(self):
+            return self
+
+        def fit(self, X, y):
+            # Fitting does not change the state of the estimator
+            return self
+
+        def fit_transform(self, X, y=None):
+            # fit_transform only transforms the data
+            return self.fitted_transformer.transform(X, y)
+
 Pipeline compatibility
 ----------------------
 For an estimator to be usable together with ``pipeline.Pipeline`` in any but the
diff --git a/doc/whats_new/v1.3.rst b/doc/whats_new/v1.3.rst
index 1cffa16a7cbe2..a0ea72c23c8a3 100644
--- a/doc/whats_new/v1.3.rst
+++ b/doc/whats_new/v1.3.rst
@@ -77,6 +77,12 @@ Changelog
     :pr:`123456` by :user:`Joe Bloggs <joeongithub>`.
     where 123456 is the *pull request* number, not the issue number.

+:mod:`sklearn.base`
+...................
+
+- |Feature| A `__sklearn_clone__` protocol is now available to override the
+  default behavior of :func:`base.clone`. :pr:`24568` by `Thomas Fan`_.
+
 :mod:`sklearn.cluster`
 ......................

diff --git a/sklearn/base.py b/sklearn/base.py
index 5a8adf1655e8e..37c61e4cb34d6 100644
--- a/sklearn/base.py
+++ b/sklearn/base.py
@@ -39,6 +39,9 @@ def clone(estimator, *, safe=True):
     without actually copying attached data. It returns a new estimator
     with the same parameters that has not been fitted on any data.

+    .. versionchanged:: 1.3
+        Delegates to `estimator.__sklearn_clone__` if the method exists.
+
     Parameters
     ----------
     estimator : {list, tuple, set} of estimator instance or a single \
@@ -46,7 +49,8 @@ def clone(estimator, *, safe=True):
         The estimator or group of estimators to be cloned.
     safe : bool, default=True
         If safe is False, clone will fall back to a deep copy on objects
-        that are not estimators.
+        that are not estimators. Ignored if `estimator.__sklearn_clone__`
+        exists.

     Returns
     -------
@@ -62,6 +66,14 @@ def clone(estimator, *, safe=True):
     return different results from the original estimator. More details can be
     found in :ref:`randomness`.
     """
+    if hasattr(estimator, "__sklearn_clone__") and not inspect.isclass(estimator):
+        return estimator.__sklearn_clone__()
+    return _clone_parametrized(estimator, safe=safe)
+
+
+def _clone_parametrized(estimator, *, safe=True):
+    """Default implementation of clone. See :func:`sklearn.base.clone` for details."""
+
     estimator_type = type(estimator)
     # XXX: not handling dictionaries
     if estimator_type in (list, tuple, set, frozenset):
@@ -219,6 +231,9 @@ def set_params(self, **params):

         return self

+    def __sklearn_clone__(self):
+        return _clone_parametrized(self)
+
     def __repr__(self, N_CHAR_MAX=700):
         # N_CHAR_MAX is the (approximate) maximum number of non-blank
         # characters to render. We pass it as an optional parameter to ease

diff --git a/sklearn/tests/test_base.py b/sklearn/tests/test_base.py
index 2a66df294b554..5e0768066b52f 100644
--- a/sklearn/tests/test_base.py
+++ b/sklearn/tests/test_base.py
@@ -6,6 +6,7 @@
 import scipy.sparse as sp
 import pytest
 import warnings
+from numpy.testing import assert_allclose

 import sklearn
 from sklearn.utils._testing import assert_array_equal
@@ -17,6 +18,7 @@
 from sklearn.preprocessing import StandardScaler
 from sklearn.utils._set_output import _get_output_config
 from sklearn.pipeline import Pipeline
+from sklearn.decomposition import PCA
 from sklearn.model_selection import GridSearchCV

 from sklearn.tree import DecisionTreeClassifier
@@ -362,6 +364,50 @@ def transform(self, X):
     assert e.scalar_param == cloned_e.scalar_param

+def test_clone_protocol():
+    """Checks that clone works with `__sklearn_clone__` protocol."""
+
+    class FrozenEstimator(BaseEstimator):
+        def __init__(self, fitted_estimator):
+            self.fitted_estimator = fitted_estimator
+
+        def __getattr__(self, name):
+            return getattr(self.fitted_estimator, name)
+
+        def __sklearn_clone__(self):
+            return self
+
+        def fit(self, *args, **kwargs):
+            return self
+
+        def fit_transform(self, *args, **kwargs):
+            return self.fitted_estimator.transform(*args, **kwargs)
+
+    X = np.array([[-1, -1], [-2, -1], [-3, -2]])
+    pca = PCA().fit(X)
+    components = pca.components_
+
+    frozen_pca = FrozenEstimator(pca)
+    assert_allclose(frozen_pca.components_, components)
+
+    # Calling PCA methods such as `get_feature_names_out` still works
+    assert_array_equal(frozen_pca.get_feature_names_out(), pca.get_feature_names_out())
+
+    # Fitting on a new data does not alter `components_`
+    X_new = np.asarray([[-1, 2], [3, 4], [1, 2]])
+    frozen_pca.fit(X_new)
+    assert_allclose(frozen_pca.components_, components)
+
+    # `fit_transform` does not alter state
+    frozen_pca.fit_transform(X_new)
+    assert_allclose(frozen_pca.components_, components)
+
+    # Cloning estimator is a no-op
+    clone_frozen_pca = clone(frozen_pca)
+    assert clone_frozen_pca is frozen_pca
+    assert_allclose(clone_frozen_pca.components_, components)
+
+
 def test_pickle_version_warning_is_not_raised_with_matching_version():
     iris = datasets.load_iris()
     tree = DecisionTreeClassifier().fit(iris.data, iris.target)

diff --git a/doc/developers/develop.rst b/doc/developers/develop.rst
index 3476e00d9..38531f66f 100644
--- a/doc/developers/develop.rst
+++ b/doc/developers/develop.rst
@@ -425,6 +425,31 @@ Objects that do not provide this method will be deep-copied
 (using the Python standard function ``copy.deepcopy``)
 if ``safe=False`` is passed to ``clone``.

+Estimators can customize the behavior of :func:`base.clone` by defining a
+`__sklearn_clone__` method. `__sklearn_clone__` must return an instance of the
+estimator. `__sklearn_clone__` is useful when an estimator needs to hold on to
+some state when :func:`base.clone` is called on the estimator. For example, a
+frozen meta-estimator for transformers can be defined as follows::
+
+    class FrozenTransformer(BaseEstimator):
+        def __init__(self, fitted_transformer):
+            self.fitted_transformer = fitted_transformer
+
+        def __getattr__(self, name):
+            # `fitted_transformer`'s attributes are now accessible
+            return getattr(self.fitted_transformer, name)
+
+        def __sklearn_clone__(self):
+            return self
+
+        def fit(self, X, y):
+            # Fitting does not change the state of the estimator
+            return self
+
+        def fit_transform(self, X, y=None):
+            # fit_transform only transforms the data
+            return self.fitted_transformer.transform(X, y)
+
 Pipeline compatibility
 ----------------------
 For an estimator to be usable together with ``pipeline.Pipeline`` in any but the

diff --git a/doc/whats_new/v1.3.rst b/doc/whats_new/v1.3.rst
index 1cffa16a7..5138fa084 100644
--- a/doc/whats_new/v1.3.rst
+++ b/doc/whats_new/v1.3.rst
@@ -77,6 +77,12 @@ Changelog
     :pr:`123456` by :user:`Joe Bloggs <joeongithub>`.
     where 123456 is the *pull request* number, not the issue number.

+:mod:`sklearn.base`
+...................
+
+- |Feature| A `__sklearn_clone__` protocol is now available to override the
+  default behavior of :func:`base.clone`.
+
 :mod:`sklearn.cluster`
 ......................


diff --git a/doc/whats_new/v1.4.rst b/doc/whats_new/v1.4.rst
index 7c42e4d3920bc..c0aaa7f839e9e 100644
--- a/doc/whats_new/v1.4.rst
+++ b/doc/whats_new/v1.4.rst
@@ -51,6 +51,9 @@ Changelog
   passed to the ``fit`` method of the the estimator. :pr:`26506` by `Adrin
   Jalali`_.

+- |Enhancement| :func:`base.clone` now supports `dict` as input and creates a
+  copy. :pr:`26786` by `Adrin Jalali`_.
+
 :mod:`sklearn.decomposition`
 ............................

@@ -85,6 +88,15 @@ Changelog
   to :ref:`metadata routing user guide <metadata_routing>`. :pr:`26789` by
   `Adrin Jalali`_.

+:mod:`sklearn.model_selection`
+..............................
+
+- |Fix| :class:`model_selection.GridSearchCV`,
+  :class:`model_selection.RandomizedSearchCV`, and
+  :class:`model_selection.HalvingGridSearchCV` now don't change the given
+  object in the parameter grid if it's an estimator. :pr:`26786` by `Adrin
+  Jalali`_.
+
 :mod:`sklearn.tree`
 ...................

diff --git a/sklearn/base.py b/sklearn/base.py
index 63b761bb17888..3e8ba86edf204 100644
--- a/sklearn/base.py
+++ b/sklearn/base.py
@@ -80,8 +80,9 @@ def _clone_parametrized(estimator, *, safe=True):
     """Default implementation of clone. See :func:`sklearn.base.clone` for details."""

     estimator_type = type(estimator)
-    # XXX: not handling dictionaries
-    if estimator_type in (list, tuple, set, frozenset):
+    if estimator_type is dict:
+        return {k: clone(v, safe=safe) for k, v in estimator.items()}
+    elif estimator_type in (list, tuple, set, frozenset):
         return estimator_type([clone(e, safe=safe) for e in estimator])
     elif not hasattr(estimator, "get_params") or isinstance(estimator, type):
         if not safe:
diff --git a/sklearn/model_selection/_search.py b/sklearn/model_selection/_search.py
index b3925865eb775..c2e94712ea97a 100644
--- a/sklearn/model_selection/_search.py
+++ b/sklearn/model_selection/_search.py
@@ -923,11 +923,14 @@ def evaluate_candidates(candidate_params, cv=None, more_results=None):
             self.best_params_ = results["params"][self.best_index_]

         if self.refit:
-            # we clone again after setting params in case some
-            # of the params are estimators as well.
-            self.best_estimator_ = clone(
-                clone(base_estimator).set_params(**self.best_params_)
+            # here we clone the estimator as well as the parameters, since
+            # sometimes the parameters themselves might be estimators, e.g.
+            # when we search over different estimators in a pipeline.
+            # ref: https://github.com/scikit-learn/scikit-learn/pull/26786
+            self.best_estimator_ = clone(base_estimator).set_params(
+                **clone(self.best_params_, safe=False)
             )
+
             refit_start_time = time.time()
             if y is not None:
                 self.best_estimator_.fit(X, y, **fit_params)
diff --git a/sklearn/model_selection/_validation.py b/sklearn/model_selection/_validation.py
index 9b66d4a67bd03..bbe184cf5cd24 100644
--- a/sklearn/model_selection/_validation.py
+++ b/sklearn/model_selection/_validation.py
@@ -726,14 +726,11 @@ def _fit_and_score(
     fit_params = _check_fit_params(X, fit_params, train)

     if parameters is not None:
-        # clone after setting parameters in case any parameters
-        # are estimators (like pipeline steps)
-        # because pipeline doesn't clone steps in fit
-        cloned_parameters = {}
-        for k, v in parameters.items():
-            cloned_parameters[k] = clone(v, safe=False)
-
-        estimator = estimator.set_params(**cloned_parameters)
+        # here we clone the parameters, since sometimes the parameters
+        # themselves might be estimators, e.g. when we search over different
+        # estimators in a pipeline.
+        # ref: https://github.com/scikit-learn/scikit-learn/pull/26786
+        estimator = estimator.set_params(**clone(parameters, safe=False))

     start_time = time.time()

diff --git a/sklearn/model_selection/tests/test_search.py b/sklearn/model_selection/tests/test_search.py
index 1bafcf3e76a4a..6ea52049f3ced 100644
--- a/sklearn/model_selection/tests/test_search.py
+++ b/sklearn/model_selection/tests/test_search.py
@@ -22,6 +22,7 @@
     make_multilabel_classification,
 )
 from sklearn.ensemble import HistGradientBoostingClassifier
+from sklearn.experimental import enable_halving_search_cv  # noqa
 from sklearn.impute import SimpleImputer
 from sklearn.linear_model import LinearRegression, Ridge, SGDClassifier
 from sklearn.metrics import (
@@ -38,6 +39,7 @@
     GridSearchCV,
     GroupKFold,
     GroupShuffleSplit,
+    HalvingGridSearchCV,
     KFold,
     LeaveOneGroupOut,
     LeavePGroupsOut,
@@ -2420,3 +2422,31 @@ def test_search_cv_verbose_3(capsys, return_train_score):
     else:
         match = re.findall(r"score=[\d\.]+", captured)
     assert len(match) == 3
+
+
+@pytest.mark.parametrize(
+    "SearchCV, param_search",
+    [
+        (GridSearchCV, "param_grid"),
+        (RandomizedSearchCV, "param_distributions"),
+        (HalvingGridSearchCV, "param_grid"),
+    ],
+)
+def test_search_estimator_param(SearchCV, param_search):
+    # test that SearchCV object doesn't change the object given in the parameter grid
+    X, y = make_classification(random_state=42)
+
+    params = {"clf": [LinearSVC(dual="auto")], "clf__C": [0.01]}
+    orig_C = params["clf"][0].C
+
+    pipe = Pipeline([("trs", MinimalTransformer()), ("clf", None)])
+
+    param_grid_search = {param_search: params}
+    gs = SearchCV(pipe, refit=True, cv=2, scoring="accuracy", **param_grid_search).fit(
+        X, y
+    )
+
+    # testing that the original object in params is not changed
+    assert params["clf"][0].C == orig_C
+    # testing that the GS is setting the parameter of the step correctly
+    assert gs.best_estimator_.named_steps["clf"].C == 0.01
diff --git a/sklearn/tests/test_base.py b/sklearn/tests/test_base.py
index 4cf50a2d1509b..8ec46e1fe9425 100644
--- a/sklearn/tests/test_base.py
+++ b/sklearn/tests/test_base.py
@@ -184,6 +184,13 @@ def test_clone_nan():
     assert clf.empty is clf2.empty

+def test_clone_dict():
+    # test that clone creates a clone of a dict
+    orig = {"a": MyEstimator()}
+    cloned = clone(orig)
+    assert orig["a"] is not cloned["a"]
+
+
 def test_clone_sparse_matrices():
     sparse_matrix_classes = [
         cls

sklearn/model_selection/tests/test_search.py::test_validate_parameter_input[input2-TypeError-Parameter (grid|distribution) for parameter 'foo' (is not|needs to be) (a list or a numpy array|iterable or a distribution).*-klass1]

sklearn/model_selection/tests/test_search.py::test_validate_parameter_input[0-TypeError-Parameter .* a dict or a list, got: 0 of type int-klass1]

sklearn/model_selection/tests/test_search.py::test_validate_parameter_input[0-TypeError-Parameter .* a dict or a list, got: 0 of type int-ParameterGrid]

sklearn/model_selection/tests/test_search.py::test_validate_parameter_input[input1-TypeError-Parameter .* is not a dict \\(0\\)-ParameterGrid]

sklearn/model_selection/tests/test_search.py::test_validate_parameter_input[input1-TypeError-Parameter .* is not a dict \\(0\\)-klass1]

sklearn/model_selection/tests/test_search.py::test_validate_parameter_input[input2-TypeError-Parameter (grid|distribution) for parameter 'foo' (is not|needs to be) (a list or a numpy array|iterable or a distribution).*-ParameterGrid]

diff --git a/doc/whats_new/v1.4.rst b/doc/whats_new/v1.4.rst
index 7c42e4d39..bd2b66c53 100644
--- a/doc/whats_new/v1.4.rst
+++ b/doc/whats_new/v1.4.rst
@@ -51,6 +51,9 @@ Changelog
   passed to the ``fit`` method of the the estimator. :pr:`26506` by `Adrin
   Jalali`_.

+- |Enhancement| :func:`base.clone` now supports `dict` as input and creates a
+  copy. :pr:`26786` by `Adrin Jalali`_.
+
 :mod:`sklearn.decomposition`
 ............................

@@ -85,6 +88,14 @@ Changelog
   to :ref:`metadata routing user guide <metadata_routing>`. :pr:`26789` by
   `Adrin Jalali`_.

+:mod:`sklearn.model_selection`
+..............................
+
+- |Fix| :class:`model_selection.GridSearchCV`,
+  :class:`model_selection.RandomizedSearchCV`, and
+  :class:`model_selection.HalvingGridSearchCV` now don't change the given
+  object in the parameter grid if it's an estimator.
+
 :mod:`sklearn.tree`
 ...................


diff --git a/.pyenchant_pylint_custom_dict.txt b/.pyenchant_pylint_custom_dict.txt
index d3cce63e24..5969ff6396 100644
--- a/.pyenchant_pylint_custom_dict.txt
+++ b/.pyenchant_pylint_custom_dict.txt
@@ -128,6 +128,7 @@ functools
 genexpr
 getattr
 globals
+globbing
 GPL
 graphname
 graphviz
diff --git a/doc/user_guide/usage/run.rst b/doc/user_guide/usage/run.rst
index 006327f402..b9dfedc886 100644
--- a/doc/user_guide/usage/run.rst
+++ b/doc/user_guide/usage/run.rst
@@ -52,6 +52,13 @@ If the analyzed sources use implicit namespace packages (PEP 420), the source ro
 be specified using the ``--source-roots`` option. Otherwise, the package names are
 detected incorrectly, since implicit namespace packages don't contain an ``__init__.py``.

+Globbing support
+----------------
+
+It is also possible to specify both directories and files using globbing patterns::
+
+   pylint [options] packages/*/src
+
 Command line options
 --------------------

diff --git a/doc/whatsnew/fragments/8310.feature b/doc/whatsnew/fragments/8310.feature
new file mode 100644
index 0000000000..a835be097c
--- /dev/null
+++ b/doc/whatsnew/fragments/8310.feature
@@ -0,0 +1,3 @@
+Support globbing pattern when defining which file/directory/module to lint.
+
+Closes #8310
diff --git a/pylint/config/config_initialization.py b/pylint/config/config_initialization.py
index d26f0e8c58..b9ac18b4d0 100644
--- a/pylint/config/config_initialization.py
+++ b/pylint/config/config_initialization.py
@@ -5,6 +5,8 @@
 from __future__ import annotations

 import sys
+from glob import glob
+from itertools import chain
 from pathlib import Path
 from typing import TYPE_CHECKING

@@ -118,6 +120,14 @@ def _config_initialization(
     # Link the base Namespace object on the current directory
     linter._directory_namespaces[Path(".").resolve()] = (linter.config, {})

-    # parsed_args_list should now only be a list of files/directories to lint.
+    # parsed_args_list should now only be a list of inputs to lint.
     # All other options have been removed from the list.
-    return parsed_args_list
+    return list(
+        chain.from_iterable(
+            # NOTE: 'or [arg]' is needed in the case the input file or directory does not exist and 'glob(arg)' cannot
+            # find anything. Without this we would not be able to output the fatal import error for this module later
+            # on, as it would get silently ignored.
+            glob(arg, recursive=True) or [arg]
+            for arg in parsed_args_list
+        )
+    )

diff --git a/tests/lint/unittest_lint.py b/tests/lint/unittest_lint.py
index 23cde49674..6c21d43135 100644
--- a/tests/lint/unittest_lint.py
+++ b/tests/lint/unittest_lint.py
@@ -1220,8 +1220,6 @@ def test_recursive_implicit_namespace() -> None:
         ],
         exit=False,
     )
-    run.linter.set_reporter(testutils.GenericTestReporter())
-    run.linter.check([join(REGRTEST_DATA_DIR, "pep420", "basic")])
     assert run.linter.file_state.base_name == "namespace.package"

@@ -1241,6 +1239,19 @@ def test_recursive_implicit_namespace_wrapper() -> None:
     assert run.linter.reporter.messages == []

+def test_globbing() -> None:
+    run = Run(
+        [
+            "--verbose",
+            "--source-roots",
+            join(REGRTEST_DATA_DIR, "pep420", "basic", "project"),
+            join(REGRTEST_DATA_DIR, "pep420", "basic", "project", "**", "__init__.py"),
+        ],
+        exit=False,
+    )
+    assert run.linter.file_state.base_name == "namespace.package.__init__"
+
+
 def test_relative_imports(initialized_linter: PyLinter) -> None:
     """Regression test for https://github.com/PyCQA/pylint/issues/3651"""
     linter = initialized_linter
diff --git a/tests/test_self.py b/tests/test_self.py
index 7dbbcf565e..cb551b0de0 100644
--- a/tests/test_self.py
+++ b/tests/test_self.py
@@ -1158,6 +1158,13 @@ def test_recursive(self) -> None:
             code=0,
         )

+    def test_recursive_globbing(self) -> None:
+        """Tests if running linter over directory using --recursive=y and globbing"""
+        self._runtest(
+            [join(HERE, "regrtest_data", "d?rectory", "subd*"), "--recursive=y"],
+            code=0,
+        )
+
     @pytest.mark.parametrize("ignore_value", ["ignored_subdirectory", "failing.py"])
     def test_ignore_recursive(self, ignore_value: str) -> None:
         """Tests recursive run of linter ignoring directory using --ignore parameter.

tests/test_self.py::TestCallbackOptions::test_output_of_callback_options[command5-Pylint global options and switches]

tests/test_self.py::TestRunTC::test_output_file_can_be_combined_with_output_format_option[text-{path}:4:4: W0612: Unused variable 'variable' (unused-variable)]

tests/test_self.py::TestRunTC::test_output_file_can_be_combined_with_output_format_option[parseable-{path}:4: [W0612(unused-variable), test] Unused variable 'variable']

tests/test_self.py::TestCallbackOptions::test_help_msg[args2---help-msg: expected at least one argumen-True]

tests/test_self.py::TestCallbackOptions::test_output_of_callback_options[command0-Emittable messages with current interpreter:]

tests/test_self.py::TestCallbackOptions::test_help_msg[args1-No such message id-False]

tests/test_self.py::TestRunTC::test_output_file_can_be_combined_with_output_format_option[colorized-{path}:4:4: W0612: \x1b[35mUnused variable 'variable'\x1b[0m (\x1b[35munused-variable\x1b[0m)]

diff --git a/doc/user_guide/usage/run.rst b/doc/user_guide/usage/run.rst
index 006327f40..b9dfedc88 100644
--- a/doc/user_guide/usage/run.rst
+++ b/doc/user_guide/usage/run.rst
@@ -52,6 +52,13 @@ If the analyzed sources use implicit namespace packages (PEP 420), the source ro
 be specified using the ``--source-roots`` option. Otherwise, the package names are
 detected incorrectly, since implicit namespace packages don't contain an ``__init__.py``.

+Globbing support
+----------------
+
+It is also possible to specify both directories and files using globbing patterns::
+
+   pylint [options] packages/*/src
+
 Command line options
 --------------------

diff --git a/doc/whatsnew/fragments/####.feature b/doc/whatsnew/fragments/####.feature
new file mode 100644
index 000000000..1407e7471
--- /dev/null
+++ b/doc/whatsnew/fragments/####.feature
@@ -0,0 +1,1 @@
+Support globbing pattern when defining which file/directory/module to lint.
\ No newline at end of file