diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 93461cdd0..3f3893823 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -98,7 +98,7 @@ repos:
     -   id: flynt
 
   - repo: https://github.com/asottile/pyupgrade
-    rev: v3.3.2
+    rev: v3.4.0
     hooks:
       - id: pyupgrade
         args: [--py36-plus]
diff --git a/nilearn/decoding/decoder.py b/nilearn/decoding/decoder.py
index 0767b5f72..63be82ade 100644
--- a/nilearn/decoding/decoder.py
+++ b/nilearn/decoding/decoder.py
@@ -304,10 +304,18 @@ def _parallel_fit(
         X_test = selector.transform(X_test)
 
     # If there is no parameter grid, then we use a suitable grid (by default)
-    param_grid = _check_param_grid(estimator, X_train, y_train, param_grid)
+    param_grid = ParameterGrid(
+        _check_param_grid(estimator, X_train, y_train, param_grid)
+    )
+
+    # collect all parameter names from the grid
+    all_params = set()
+    for params in param_grid:
+        all_params.update(params.keys())
+
     best_score = None
-    for param in ParameterGrid(param_grid):
-        estimator = clone(estimator).set_params(**param)
+    for params in param_grid:
+        estimator = clone(estimator).set_params(**params)
         estimator.fit(X_train, y_train)
 
         if is_classification:
@@ -332,8 +340,13 @@ def _parallel_fit(
                     dummy_output = estimator.constant_
 
             if isinstance(estimator, (RidgeCV, RidgeClassifierCV)):
-                param["best_alpha"] = estimator.alpha_
-            best_param = param
+                params["best_alpha"] = estimator.alpha_
+            best_params = params
+
+            # fill in any missing param from param_grid
+            for param in all_params:
+                if param not in best_params:
+                    best_params[param] = getattr(estimator, param)
 
     if best_coef is not None:
         if do_screening:
@@ -346,7 +359,7 @@ def _parallel_fit(
         class_index,
         best_coef,
         best_intercept,
-        best_param,
+        best_params,
         best_score,
         dummy_output,
     )

diff --git a/doc/changes/latest.rst b/doc/changes/latest.rst
index bb4253eb1..2150f15d0 100644
--- a/doc/changes/latest.rst
+++ b/doc/changes/latest.rst
@@ -11,6 +11,8 @@ NEW
 Fixes
 -----
 
+- :bdg-dark:`Code` Fix bug where the `cv_params_` attribute of fitter Decoder objects sometimes had missing entries if `grid_param` is a sequence of dicts with different keys (:gh:`3733` by `Michelle Wang`_).
+
 Enhancements
 ------------
 
diff --git a/nilearn/decoding/tests/test_decoder.py b/nilearn/decoding/tests/test_decoder.py
index b97b4d0fb..c8dd1c728 100644
--- a/nilearn/decoding/tests/test_decoder.py
+++ b/nilearn/decoding/tests/test_decoder.py
@@ -441,6 +441,31 @@ def test_parallel_fit_builtin_cv(
     assert isinstance(best_param[fitted_param_name], (float, int))
 
 
+def test_decoder_param_grid_sequence(binary_classification_data):
+    X, y, _ = binary_classification_data
+    n_cv_folds = 10
+    param_grid = [
+        {
+            "penalty": ["l2"],
+            "C": [100, 1000],
+            "random_state": [42],  # fix the seed for consistent behaviour
+        },
+        {
+            "penalty": ["l1"],
+            "dual": [False],  # "dual" is not in the first dict
+            "C": [100, 10],
+            "random_state": [42],  # fix the seed for consistent behaviour
+        },
+    ]
+
+    model = Decoder(param_grid=param_grid, cv=n_cv_folds)
+    model.fit(X, y)
+
+    for best_params in model.cv_params_.values():
+        for param_list in best_params.values():
+            assert len(param_list) == n_cv_folds
+
+
 def test_decoder_binary_classification_with_masker_object(
     binary_classification_data,
 ):

[BUG]: missing values in `Decoder.cv_params_` when `param_grid` is a list of dicts with different keys
If `param_grid` is a list of dicts and the dicts have different keys, the fitted Decoder's `cv_params_` output can contain lists that have less elements than the number of CV folds. For example, if `param_grid` is
```python
param_grid = [
    {
        "penalty": ["l2"],
        "C": [100, 1000],
    },
    {
        "penalty": ["l1"],
        "dual": [False],    # "dual" is not in the first dict
        "C": [100, 1000],
    },
]
```

Then it is possible that `decoder.cv_params_["shoe"]` has something like
```python
{
    'C': [1000, 1000, 100, 100, 1000], 
    'dual': [False, False, False],    # less than 5, not aligned with the other hyperparameter values
    'penalty': ['l1', 'l2', 'l1', 'l1', 'l2'],
}
```

_Originally posted by @michellewang in https://github.com/nilearn/nilearn/pull/3626#discussion_r1172699079_
            
nilearn/decoding/tests/test_decoder.py::test_decoder_param_grid_sequence
nilearn/decoding/tests/test_decoder.py::test_check_param_grid_regression[regressor0-param0] nilearn/decoding/tests/test_decoder.py::test_check_param_grid_regression[regressor1-C] nilearn/decoding/tests/test_decoder.py::test_check_param_grid_classification[classifier0-C] nilearn/decoding/tests/test_decoder.py::test_check_param_grid_classification[classifier1-C] nilearn/decoding/tests/test_decoder.py::test_check_param_grid_classification[classifier2-param2] nilearn/decoding/tests/test_decoder.py::test_non_supported_estimator_error[log_l1] nilearn/decoding/tests/test_decoder.py::test_non_supported_estimator_error[estimator1] nilearn/decoding/tests/test_decoder.py::test_check_parameter_grid_is_empty nilearn/decoding/tests/test_decoder.py::test_wrap_param_grid[param_grid0] nilearn/decoding/tests/test_decoder.py::test_wrap_param_grid[param_grid1] nilearn/decoding/tests/test_decoder.py::test_wrap_param_grid[param_grid2] nilearn/decoding/tests/test_decoder.py::test_wrap_param_grid[param_grid3] nilearn/decoding/tests/test_decoder.py::test_wrap_param_grid[param_grid4] nilearn/decoding/tests/test_decoder.py::test_wrap_param_grid[param_grid5] nilearn/decoding/tests/test_decoder.py::test_wrap_param_grid[param_grid6] nilearn/decoding/tests/test_decoder.py::test_wrap_param_grid[param_grid7] nilearn/decoding/tests/test_decoder.py::test_wrap_param_grid[param_grid8] nilearn/decoding/tests/test_decoder.py::test_wrap_param_grid[param_grid9] nilearn/decoding/tests/test_decoder.py::test_wrap_param_grid[param_grid10] nilearn/decoding/tests/test_decoder.py::test_wrap_param_grid_warning[param_grid0-True] nilearn/decoding/tests/test_decoder.py::test_wrap_param_grid_warning[param_grid1-False] nilearn/decoding/tests/test_decoder.py::test_wrap_param_grid_is_none nilearn/decoding/tests/test_decoder.py::test_check_inputs_length[DecoderRegressor] nilearn/decoding/tests/test_decoder.py::test_check_inputs_length[Decoder] nilearn/decoding/tests/test_decoder.py::test_check_inputs_length[FREMRegressor] nilearn/decoding/tests/test_decoder.py::test_check_inputs_length[FREMClassifier] nilearn/decoding/tests/test_decoder.py::test_check_supported_estimator[svc] nilearn/decoding/tests/test_decoder.py::test_check_supported_estimator[svc_l2] nilearn/decoding/tests/test_decoder.py::test_check_supported_estimator[svc_l1] nilearn/decoding/tests/test_decoder.py::test_check_supported_estimator[logistic] nilearn/decoding/tests/test_decoder.py::test_check_supported_estimator[logistic_l1] nilearn/decoding/tests/test_decoder.py::test_check_supported_estimator[logistic_l2] nilearn/decoding/tests/test_decoder.py::test_check_supported_estimator[ridge] nilearn/decoding/tests/test_decoder.py::test_check_supported_estimator[ridge_classifier] nilearn/decoding/tests/test_decoder.py::test_check_supported_estimator[ridge_regressor] nilearn/decoding/tests/test_decoder.py::test_check_supported_estimator[svr] nilearn/decoding/tests/test_decoder.py::test_check_supported_estimator[dummy_classifier] nilearn/decoding/tests/test_decoder.py::test_check_supported_estimator[dummy_regressor] nilearn/decoding/tests/test_decoder.py::test_check_unsupported_estimator[ridgo] nilearn/decoding/tests/test_decoder.py::test_check_unsupported_estimator[svb] nilearn/decoding/tests/test_decoder.py::test_parallel_fit nilearn/decoding/tests/test_decoder.py::test_parallel_fit_builtin_cv[estimator0-alphas-best_alpha-False-param_values0] nilearn/decoding/tests/test_decoder.py::test_parallel_fit_builtin_cv[estimator0-alphas-best_alpha-False-param_values1] nilearn/decoding/tests/test_decoder.py::test_parallel_fit_builtin_cv[estimator1-alphas-best_alpha-True-param_values0] nilearn/decoding/tests/test_decoder.py::test_parallel_fit_builtin_cv[estimator1-alphas-best_alpha-True-param_values1] nilearn/decoding/tests/test_decoder.py::test_decoder_binary_classification_with_logistic_model nilearn/decoding/tests/test_decoder.py::test_decoder_binary_classification_screening[100] nilearn/decoding/tests/test_decoder.py::test_decoder_binary_classification_screening[20] nilearn/decoding/tests/test_decoder.py::test_decoder_binary_classification_screening[None] nilearn/decoding/tests/test_decoder.py::test_decoder_binary_classification_clustering[100] nilearn/decoding/tests/test_decoder.py::test_decoder_binary_classification_clustering[99] nilearn/decoding/tests/test_decoder.py::test_decoder_binary_classification_cross_validation[cv0] nilearn/decoding/tests/test_decoder.py::test_decoder_binary_classification_cross_validation[cv1] nilearn/decoding/tests/test_decoder.py::test_decoder_dummy_classifier nilearn/decoding/tests/test_decoder.py::test_decoder_dummy_classifier_with_callable nilearn/decoding/tests/test_decoder.py::test_decoder_error_model_not_fitted nilearn/decoding/tests/test_decoder.py::test_decoder_dummy_classifier_strategy_prior nilearn/decoding/tests/test_decoder.py::test_decoder_dummy_classifier_strategy_most_frequent nilearn/decoding/tests/test_decoder.py::test_decoder_dummy_classifier_roc_scoring nilearn/decoding/tests/test_decoder.py::test_decoder_error_not_implemented nilearn/decoding/tests/test_decoder.py::test_decoder_dummy_classifier_default_scoring nilearn/decoding/tests/test_decoder.py::test_decoder_classification_string_label nilearn/decoding/tests/test_decoder.py::test_decoder_regression_screening[ridge-100] nilearn/decoding/tests/test_decoder.py::test_decoder_regression_screening[ridge-20] nilearn/decoding/tests/test_decoder.py::test_decoder_regression_screening[ridge-1] nilearn/decoding/tests/test_decoder.py::test_decoder_regression_screening[ridge-None] nilearn/decoding/tests/test_decoder.py::test_decoder_regression_screening[svr-100] nilearn/decoding/tests/test_decoder.py::test_decoder_regression_screening[svr-20] nilearn/decoding/tests/test_decoder.py::test_decoder_regression_screening[svr-1] nilearn/decoding/tests/test_decoder.py::test_decoder_regression_screening[svr-None] nilearn/decoding/tests/test_decoder.py::test_decoder_regression_clustering[ridge-100] nilearn/decoding/tests/test_decoder.py::test_decoder_regression_clustering[ridge-99] nilearn/decoding/tests/test_decoder.py::test_decoder_regression_clustering[svr-100] nilearn/decoding/tests/test_decoder.py::test_decoder_regression_clustering[svr-99] nilearn/decoding/tests/test_decoder.py::test_decoder_dummy_regression nilearn/decoding/tests/test_decoder.py::test_decoder_dummy_regression_default_scoring_metric_is_r2 nilearn/decoding/tests/test_decoder.py::test_decoder_dummy_regression_other_strategy nilearn/decoding/tests/test_decoder.py::test_decoder_multiclass_classification_masker nilearn/decoding/tests/test_decoder.py::test_decoder_multiclass_classification_masker_dummy_classifier nilearn/decoding/tests/test_decoder.py::test_decoder_multiclass_classification_screening[100] nilearn/decoding/tests/test_decoder.py::test_decoder_multiclass_classification_screening[20] nilearn/decoding/tests/test_decoder.py::test_decoder_multiclass_classification_screening[None] nilearn/decoding/tests/test_decoder.py::test_decoder_multiclass_classification_clustering[svc_l2-100] nilearn/decoding/tests/test_decoder.py::test_decoder_multiclass_classification_clustering[svc_l2-99] nilearn/decoding/tests/test_decoder.py::test_decoder_multiclass_classification_clustering[svc_l1-100] nilearn/decoding/tests/test_decoder.py::test_decoder_multiclass_classification_clustering[svc_l1-99] nilearn/decoding/tests/test_decoder.py::test_decoder_multiclass_classification_cross_validation[cv0] nilearn/decoding/tests/test_decoder.py::test_decoder_multiclass_classification_cross_validation[cv1] nilearn/decoding/tests/test_decoder.py::test_decoder_multiclass_classification_apply_mask_shape nilearn/decoding/tests/test_decoder.py::test_decoder_multiclass_classification_apply_mask_attributes nilearn/decoding/tests/test_decoder.py::test_decoder_multiclass_error_incorrect_cv nilearn/decoding/tests/test_decoder.py::test_decoder_multiclass_warnings