diff --git a/doc/run_experiment.rst b/doc/run_experiment.rst
index 652966ed..a3cb82d6 100644
--- a/doc/run_experiment.rst
+++ b/doc/run_experiment.rst
@@ -1015,106 +1015,106 @@ to be included in the model. Defaults to 1.
 param_grids *(Optional)*
 """"""""""""""""""""""""
 
-List of parameter grids to search for each learner. Each parameter
-grid should be a list of dictionaries mapping from strings to lists
-of parameter values. When you specify an empty list for a learner,
-the default parameter grid for that learner will be searched.
+List of parameter grid dictionaries, one for each learner. Each parameter
+grid is a dictionary mapping from strings to list of parameter values. When you
+specify an empty dictionary for a learner, the default parameter grid for that
+learner will be searched.
 
 The default parameter grids for each learner are:
 
 AdaBoostClassifier and AdaBoostRegressor
     .. code-block:: python
 
-        [{'learning_rate': [0.01, 0.1, 1.0, 10.0, 100.0]}]
+        {'learning_rate': [0.01, 0.1, 1.0, 10.0, 100.0]}
 
 BayesianRidge
     .. code-block:: python
 
-        [{'alpha_1': [1e-6, 1e-4, 1e-2, 1, 10],
-          'alpha_2': [1e-6, 1e-4, 1e-2, 1, 10],
-          'lambda_1': [1e-6, 1e-4, 1e-2, 1, 10],
-          'lambda_2': [1e-6, 1e-4, 1e-2, 1, 10]}]
+        {'alpha_1': [1e-6, 1e-4, 1e-2, 1, 10],
+         'alpha_2': [1e-6, 1e-4, 1e-2, 1, 10],
+         'lambda_1': [1e-6, 1e-4, 1e-2, 1, 10],
+         'lambda_2': [1e-6, 1e-4, 1e-2, 1, 10]}
 
 DecisionTreeClassifier and DecisionTreeRegressor
     .. code-block:: python
 
-       [{'max_features': ["auto", None]}]
+       {'max_features': ["auto", None]}
 
 ElasticNet
     .. code-block:: python
 
-       [{'alpha': [0.01, 0.1, 1.0, 10.0, 100.0]}]
+       {'alpha': [0.01, 0.1, 1.0, 10.0, 100.0]}
 
 GradientBoostingClassifier and GradientBoostingRegressor
     .. code-block:: python
 
-       [{'max_depth': [1, 3, 5]}]
+       {'max_depth': [1, 3, 5]}
 
 HuberRegressor
     .. code-block:: python
 
-        [{'epsilon': [1.05, 1.35, 1.5, 2.0, 2.5, 5.0],
-          'alpha': [1e-4, 1e-3, 1e-3, 1e-1, 1, 10, 100, 1000]}]
+        {'epsilon': [1.05, 1.35, 1.5, 2.0, 2.5, 5.0],
+         'alpha': [1e-4, 1e-3, 1e-3, 1e-1, 1, 10, 100, 1000]}
 
 KNeighborsClassifier and KNeighborsRegressor
     .. code-block:: python
 
-        [{'n_neighbors': [1, 5, 10, 100],
-          'weights': ['uniform', 'distance']}]
+        {'n_neighbors': [1, 5, 10, 100],
+         'weights': ['uniform', 'distance']}
 
 Lasso
     .. code-block:: python
 
-       [{'alpha': [0.01, 0.1, 1.0, 10.0, 100.0]}]
+       {'alpha': [0.01, 0.1, 1.0, 10.0, 100.0]}
 
 LinearSVC
     .. code-block:: python
 
-       [{'C': [0.01, 0.1, 1.0, 10.0, 100.0]}]
+       {'C': [0.01, 0.1, 1.0, 10.0, 100.0]}
 
 LogisticRegression
     .. code-block:: python
 
-       [{'C': [0.01, 0.1, 1.0, 10.0, 100.0]}]
+       {'C': [0.01, 0.1, 1.0, 10.0, 100.0]}
 
 MLPClassifier and MLPRegressor:
     .. code-block:: python
 
-       [{'activation': ['logistic', 'tanh', 'relu'],
-         'alpha': [1e-4, 1e-3, 1e-3, 1e-1, 1],
-         'learning_rate_init': [0.001, 0.01, 0.1]}],
+       {'activation': ['logistic', 'tanh', 'relu'],
+        'alpha': [1e-4, 1e-3, 1e-3, 1e-1, 1],
+        'learning_rate_init': [0.001, 0.01, 0.1]},
 
 MultinomialNB
     .. code-block:: python
 
-       [{'alpha': [0.1, 0.25, 0.5, 0.75, 1.0]}]
+       {'alpha': [0.1, 0.25, 0.5, 0.75, 1.0]}
 
 RandomForestClassifier and RandomForestRegressor
     .. code-block:: python
 
-       [{'max_depth': [1, 5, 10, None]}]
+       {'max_depth': [1, 5, 10, None]}
 
 Ridge and RidgeClassifier
     .. code-block:: python
 
-       [{'alpha': [0.01, 0.1, 1.0, 10.0, 100.0]}]
+       {'alpha': [0.01, 0.1, 1.0, 10.0, 100.0]}
 
 SGDClassifier and SGDRegressor
     .. code-block:: python
 
-        [{'alpha': [0.000001, 0.00001, 0.0001, 0.001, 0.01],
-          'penalty': ['l1', 'l2', 'elasticnet']}]
+        {'alpha': [0.000001, 0.00001, 0.0001, 0.001, 0.01],
+         'penalty': ['l1', 'l2', 'elasticnet']}
 
 SVC
     .. code-block:: python
 
-       [{'C': [0.01, 0.1, 1.0, 10.0, 100.0],
-         'gamma': ['auto', 0.01, 0.1, 1.0, 10.0, 100.0]}]
+       {'C': [0.01, 0.1, 1.0, 10.0, 100.0],
+        'gamma': ['auto', 0.01, 0.1, 1.0, 10.0, 100.0]}
 
 SVR
     .. code-block:: python
 
-       [{'C': [0.01, 0.1, 1.0, 10.0, 100.0]}]
+       {'C': [0.01, 0.1, 1.0, 10.0, 100.0]}
 
 .. note::
     Note that learners not listed here do not have any default
diff --git a/skll/config/__init__.py b/skll/config/__init__.py
index d5ea7096..fe3779e8 100644
--- a/skll/config/__init__.py
+++ b/skll/config/__init__.py
@@ -330,7 +330,7 @@ def parse_config_file(config_path, log_level=logging.INFO):
         List of dicts containing parameters you want to have fixed for
         each classifier in learners list.
     param_grid_list : list of dict
-        List of parameter grids to search for each learner.
+        List of parameter grids to search, one dict for each learner.
     featureset_names : list of str
         The names of the featuresets used for each job.
     learners : list of str
diff --git a/skll/learner/__init__.py b/skll/learner/__init__.py
index 6d7086d2..9dacaca5 100644
--- a/skll/learner/__init__.py
+++ b/skll/learner/__init__.py
@@ -198,7 +198,7 @@ class Learner(object):
             model_class = globals()[model_type]
             default_param_grid = (model_class.default_param_grid()
                                   if hasattr(model_class, 'default_param_grid')
-                                  else [{}])
+                                  else {})
 
             # ewww, globals :-(
             global _REQUIRES_DENSE
@@ -600,7 +600,7 @@ class Learner(object):
         -------
         estimator
             The estimator that was created.
-        default_param_grid : list of dicts
+        default_param_grid : dict
             The parameter grid for the estimator.
 
         Raises
@@ -759,7 +759,7 @@ class Learner(object):
         ----------
         examples : skll.FeatureSet
             The ``FeatureSet`` instance to use for training.
-        param_grid : list of dicts, optional
+        param_grid : dict, optional
             The parameter grid to search through for grid
             search. If ``None``, a default parameter grid
             will be used.
@@ -939,7 +939,7 @@ class Learner(object):
         # In case the default parameter grid is also empty
         # then there's no point doing the grid search at all
         if grid_search and not param_grid:
-            if default_param_grid == [{}]:
+            if default_param_grid == {}:
                 self.logger.warning("SKLL has no default parameter grid "
                                     "available for the {} learner and no "
                                     "parameter grids were supplied. Using "
@@ -1045,7 +1045,7 @@ class Learner(object):
                 else:
                     self.logger.warning("A custom pipeline stage "
                                         "(`Densifier`) will be inserted "
-                                        " in the pipeline since the "
+                                        "in the pipeline since the "
                                         "current SKLL configuration "
                                         "requires dense features.")
                     densifier = Densifier()
@@ -1567,8 +1567,8 @@ class Learner(object):
             prefix that will be used for the filename.
             It will be followed by ``"_predictions.tsv"``
             Defaults to ``None``.
-        param_grid : list of dicts, optional
-            The parameter grid to traverse.
+        param_grid : dict, optional
+            The parameter grid to search.
             Defaults to ``None``.
         shuffle : bool, optional
             Shuffle examples before splitting into folds for CV.
diff --git a/skll/utils/constants.py b/skll/utils/constants.py
index aee720dd..bc8f31d8 100644
--- a/skll/utils/constants.py
+++ b/skll/utils/constants.py
@@ -38,83 +38,83 @@ from sklearn.svm import LinearSVC, SVC, LinearSVR, SVR
 from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor
 
 KNOWN_DEFAULT_PARAM_GRIDS = {AdaBoostClassifier:
-                             [{'learning_rate': [0.01, 0.1, 1.0, 10.0, 100.0]}],
+                             {'learning_rate': [0.01, 0.1, 1.0, 10.0, 100.0]},
                              AdaBoostRegressor:
-                             [{'learning_rate': [0.01, 0.1, 1.0, 10.0, 100.0]}],
+                             {'learning_rate': [0.01, 0.1, 1.0, 10.0, 100.0]},
                              BayesianRidge:
-                             [{'alpha_1': [1e-6, 1e-4, 1e-2, 1, 10],
-                               'alpha_2': [1e-6, 1e-4, 1e-2, 1, 10],
-                               'lambda_1': [1e-6, 1e-4, 1e-2, 1, 10],
-                               'lambda_2': [1e-6, 1e-4, 1e-2, 1, 10]}],
+                             {'alpha_1': [1e-6, 1e-4, 1e-2, 1, 10],
+                              'alpha_2': [1e-6, 1e-4, 1e-2, 1, 10],
+                              'lambda_1': [1e-6, 1e-4, 1e-2, 1, 10],
+                              'lambda_2': [1e-6, 1e-4, 1e-2, 1, 10]},
                              DecisionTreeClassifier:
-                             [{'max_features': ["auto", None]}],
+                             {'max_features': ["auto", None]},
                              DecisionTreeRegressor:
-                             [{'max_features': ["auto", None]}],
+                             {'max_features': ["auto", None]},
                              DummyClassifier:
-                             [{}],
+                             {},
                              DummyRegressor:
-                             [{}],
+                             {},
                              ElasticNet:
-                             [{'alpha': [0.01, 0.1, 1.0, 10.0, 100.0]}],
+                             {'alpha': [0.01, 0.1, 1.0, 10.0, 100.0]},
                              GradientBoostingClassifier:
-                             [{'max_depth': [1, 3, 5]}],
+                             {'max_depth': [1, 3, 5]},
                              GradientBoostingRegressor:
-                             [{'max_depth': [1, 3, 5]}],
+                             {'max_depth': [1, 3, 5]},
                              HuberRegressor:
-                             [{'epsilon': [1.05, 1.35, 1.5, 2.0, 2.5, 5.0],
-                               'alpha': [1e-4, 1e-3, 1e-2, 1e-1, 1, 10, 100, 1000]}],
+                             {'epsilon': [1.05, 1.35, 1.5, 2.0, 2.5, 5.0],
+                              'alpha': [1e-4, 1e-3, 1e-2, 1e-1, 1, 10, 100, 1000]},
                              KNeighborsClassifier:
-                             [{'n_neighbors': [1, 5, 10, 100],
-                               'weights': ['uniform', 'distance']}],
+                             {'n_neighbors': [1, 5, 10, 100],
+                              'weights': ['uniform', 'distance']},
                              KNeighborsRegressor:
-                             [{'n_neighbors': [1, 5, 10, 100],
-                               'weights': ['uniform', 'distance']}],
+                             {'n_neighbors': [1, 5, 10, 100],
+                              'weights': ['uniform', 'distance']},
                              MLPClassifier:
-                             [{'activation': ['logistic', 'tanh', 'relu'],
-                               'alpha': [1e-4, 1e-3, 1e-2, 1e-1, 1],
-                               'learning_rate_init': [0.001, 0.01, 0.1]}],
+                             {'activation': ['logistic', 'tanh', 'relu'],
+                              'alpha': [1e-4, 1e-3, 1e-2, 1e-1, 1],
+                              'learning_rate_init': [0.001, 0.01, 0.1]},
                              MLPRegressor:
-                             [{'activation': ['logistic', 'tanh', 'relu'],
-                               'alpha': [1e-4, 1e-3, 1e-2, 1e-1, 1],
-                               'learning_rate_init': [0.001, 0.01, 0.1]}],
+                             {'activation': ['logistic', 'tanh', 'relu'],
+                              'alpha': [1e-4, 1e-3, 1e-2, 1e-1, 1],
+                              'learning_rate_init': [0.001, 0.01, 0.1]},
                              MultinomialNB:
-                             [{'alpha': [0.1, 0.25, 0.5, 0.75, 1.0]}],
+                             {'alpha': [0.1, 0.25, 0.5, 0.75, 1.0]},
                              Lars:
-                             [{}],
+                             {},
                              Lasso:
-                             [{'alpha': [0.01, 0.1, 1.0, 10.0, 100.0]}],
+                             {'alpha': [0.01, 0.1, 1.0, 10.0, 100.0]},
                              LinearRegression:
-                             [{}],
+                             {},
                              LinearSVC:
-                             [{'C': [0.01, 0.1, 1.0, 10.0, 100.0]}],
+                             {'C': [0.01, 0.1, 1.0, 10.0, 100.0]},
                              LogisticRegression:
-                             [{'C': [0.01, 0.1, 1.0, 10.0, 100.0]}],
+                             {'C': [0.01, 0.1, 1.0, 10.0, 100.0]},
                              SVC:
-                             [{'C': [0.01, 0.1, 1.0, 10.0, 100.0],
-                               'gamma': ['auto', 'scale', 0.01, 0.1, 1.0, 10.0, 100.0]}],
+                             {'C': [0.01, 0.1, 1.0, 10.0, 100.0],
+                              'gamma': ['auto', 'scale', 0.01, 0.1, 1.0, 10.0, 100.0]},
                              RandomForestClassifier:
-                             [{'max_depth': [1, 5, 10, None]}],
+                             {'max_depth': [1, 5, 10, None]},
                              RandomForestRegressor:
-                             [{'max_depth': [1, 5, 10, None]}],
+                             {'max_depth': [1, 5, 10, None]},
                              RANSACRegressor:
-                             [{}],
+                             {},
                              Ridge:
-                             [{'alpha': [0.01, 0.1, 1.0, 10.0, 100.0]}],
+                             {'alpha': [0.01, 0.1, 1.0, 10.0, 100.0]},
                              RidgeClassifier:
-                             [{'alpha': [0.01, 0.1, 1.0, 10.0, 100.0]}],
+                             {'alpha': [0.01, 0.1, 1.0, 10.0, 100.0]},
                              SGDClassifier:
-                             [{'alpha': [0.000001, 0.00001, 0.0001, 0.001, 0.01],
-                               'penalty': ['l1', 'l2', 'elasticnet']}],
+                             {'alpha': [0.000001, 0.00001, 0.0001, 0.001, 0.01],
+                              'penalty': ['l1', 'l2', 'elasticnet']},
                              SGDRegressor:
-                             [{'alpha': [0.000001, 0.00001, 0.0001, 0.001, 0.01],
-                               'penalty': ['l1', 'l2', 'elasticnet']}],
+                             {'alpha': [0.000001, 0.00001, 0.0001, 0.001, 0.01],
+                              'penalty': ['l1', 'l2', 'elasticnet']},
                              LinearSVR:
-                             [{'C': [0.01, 0.1, 1.0, 10.0, 100.0]}],
+                             {'C': [0.01, 0.1, 1.0, 10.0, 100.0]},
                              SVR:
-                             [{'C': [0.01, 0.1, 1.0, 10.0, 100.0],
-                               'gamma': ['auto', 'scale', 0.01, 0.1, 1.0, 10.0, 100.0]}],
+                             {'C': [0.01, 0.1, 1.0, 10.0, 100.0],
+                              'gamma': ['auto', 'scale', 0.01, 0.1, 1.0, 10.0, 100.0]},
                              TheilSenRegressor:
-                             [{}]
+                             {}
                              }
 
 KNOWN_REQUIRES_DENSE = (BayesianRidge, Lars, TheilSenRegressor)

Simplify parameter grids to be dictionaries instead of lists of dictionaries
Right now, we always use a list with a single dictionary to represent the default parameter grid for a learner. However, [`GridSearchCV`](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.GridSearchCV.html) can take a plain dictionary as well. Given that we almost never search multiple grids, switching to a plain dictionary will make things a lot simpler when implementing voting and stacking learners. 
diff --git a/tests/test_classification.py b/tests/test_classification.py
index e963c064..c08d11a8 100644
--- a/tests/test_classification.py
+++ b/tests/test_classification.py
@@ -420,8 +420,7 @@ def test_default_param_grids_no_duplicates():
     """
     Verify that the default parameter grids don't contain duplicate values.
     """
-    for learner, param_list in KNOWN_DEFAULT_PARAM_GRIDS.items():
-        param_dict = param_list[0]
+    for learner, param_dict in KNOWN_DEFAULT_PARAM_GRIDS.items():
         for param_name, values in param_dict.items():
             assert(len(set(values)) == len(values))
 

pip install -e .
parse_log_pytest
requirements.txt
nose nose-cov pytest
apt-get update apt-get install -y gcc
3.7
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
beautifulsoup4==4.13.3
certifi @ file:///croot/certifi_1671487769961/work/certifi
cov-core==1.15.0
coverage==7.2.7
cycler==0.11.0
exceptiongroup==1.2.2
fonttools==4.38.0
importlib-metadata==6.7.0
iniconfig==2.0.0
joblib==1.3.2
kiwisolver==1.4.5
matplotlib==3.5.3
nose==1.3.7
nose-cov==1.6
numpy==1.21.6
packaging==24.0
pandas==1.3.5
Pillow==9.5.0
pluggy==1.2.0
pyparsing==3.1.4
pytest==7.4.4
python-dateutil==2.9.0.post0
pytz==2025.2
ruamel.yaml==0.18.10
ruamel.yaml.clib==0.2.8
scikit-learn==0.23.1
scipy==1.7.3
seaborn==0.12.2
six==1.17.0
-e git+https://github.com/EducationalTestingService/skll.git@6bf31ab3cdaf42d56c739b7ff2a621704bfe86c0#egg=skll
soupsieve==2.4.1
tabulate==0.9.0
threadpoolctl==3.1.0
tomli==2.0.1
typing_extensions==4.7.1
zipp==3.15.0

name: skll
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - beautifulsoup4==4.13.3
      - cov-core==1.15.0
      - coverage==7.2.7
      - cycler==0.11.0
      - exceptiongroup==1.2.2
      - fonttools==4.38.0
      - importlib-metadata==6.7.0
      - iniconfig==2.0.0
      - joblib==1.3.2
      - kiwisolver==1.4.5
      - matplotlib==3.5.3
      - nose==1.3.7
      - nose-cov==1.6
      - numpy==1.21.6
      - packaging==24.0
      - pandas==1.3.5
      - pillow==9.5.0
      - pluggy==1.2.0
      - pyparsing==3.1.4
      - pytest==7.4.4
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - ruamel-yaml==0.18.10
      - ruamel-yaml-clib==0.2.8
      - scikit-learn==0.23.1
      - scipy==1.7.3
      - seaborn==0.12.2
      - six==1.17.0
      - soupsieve==2.4.1
      - tabulate==0.9.0
      - threadpoolctl==3.1.0
      - tomli==2.0.1
      - typing-extensions==4.7.1
      - zipp==3.15.0
prefix: /opt/conda/envs/skll

tests/test_classification.py::test_default_param_grids_no_duplicates
tests/test_classification.py::test_train_file_test_file tests/test_classification.py::test_predict_on_subset_with_existing_model tests/test_classification.py::test_train_file_test_file_ablation tests/test_classification.py::test_train_file_and_train_directory tests/test_classification.py::test_test_file_and_test_directory
tests/test_classification.py::test_contiguous_int_or_float_labels tests/test_classification.py::test_label_index_order tests/test_classification.py::test_predict_dict_dict tests/test_classification.py::test_predict_hasher_hasher_same_bins tests/test_classification.py::test_predict_hasher_hasher_different_bins tests/test_classification.py::test_predict_hasher_dict tests/test_classification.py::test_predict_dict_hasher tests/test_classification.py::test_rare_class tests/test_classification.py::test_mlp_classification tests/test_classification.py::test_xval_float_classes_as_strings tests/test_classification.py::test_learner_api_load_into_existing_instance tests/test_classification.py::test_hashing_for_multinomialNB tests/test_classification.py::test_sampling_for_multinomialNB tests/test_classification.py::test_multinomialnb_loading tests/test_classification.py::test_load_old_skll_model
skll/config/__init__.py skll/learner/__init__.py doc/run_experiment.rst skll/utils/constants.py
skll/config/__init__.py skll/learner/__init__.py doc/run_experiment.rst skll/utils/constants.py