Repository: EducationalTestingService/rsmtool
Pull request #683 (2024-02-12 23:25:28)

Add Type Hints (Final Part)
### Changes

- Add intersphinx mapping for prompt_toolkit
- Add type hints for `utils.commandline`
- Add type hints for `utils.conversion`
- Add type hints for `utils.cross_validation`
- Add type hints for `utils.files`
- Add type hints for `utils.logging`
- Add type hints for `utils.metrics`
- Add type hints for `utils.models`
- Add type hints for `utils.notebook`
- Add type hints for `utils.prmse`.
- Add type hints for `utils.wandb`

### How to review
- Please check that I have not left any parameters/arguments/return values without type hints.
- Please check that the types in the docstrings match those in the actual code. 
- Here's the [RTD build for this branch](https://rsmtool.readthedocs.io/en/326-type-hints-5). Check the "API Documentation" section (for the files affected by this MR) to confirm that all types are linked to relevant pages from external documentations (e.g., Python, Numpy, SKLL, stats models etc.) and there are no typos etc.
- Check for typos and/or weird issues in docstrings.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/EducationalTestingService/rsmtool/pull/683?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EducationalTestingService) Report
Attention: `2 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`32b91b7`)](https://app.codecov.io/gh/EducationalTestingService/rsmtool/commit/32b91b79c508b5753c9bb915b8170575cdf65f2c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EducationalTestingService) 96.10% compared to head [(`822da5f`)](https://app.codecov.io/gh/EducationalTestingService/rsmtool/pull/683?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EducationalTestingService) 96.10%.

| [Files](https://app.codecov.io/gh/EducationalTestingService/rsmtool/pull/683?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EducationalTestingService) | Patch % | Lines |
|---|---|---|
| [rsmtool/utils/cross\_validation.py](https://app.codecov.io/gh/EducationalTestingService/rsmtool/pull/683?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EducationalTestingService#diff-cnNtdG9vbC91dGlscy9jcm9zc192YWxpZGF0aW9uLnB5) | 85.71% | [1 Missing :warning: ](https://app.codecov.io/gh/EducationalTestingService/rsmtool/pull/683?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EducationalTestingService) |
| [rsmtool/utils/prmse.py](https://app.codecov.io/gh/EducationalTestingService/rsmtool/pull/683?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EducationalTestingService#diff-cnNtdG9vbC91dGlscy9wcm1zZS5weQ==) | 88.88% | [1 Missing :warning: ](https://app.codecov.io/gh/EducationalTestingService/rsmtool/pull/683?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EducationalTestingService) |

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main     #683   +/-   ##
=======================================
  Coverage   96.10%   96.10%           
=======================================
  Files          32       32           
  Lines        4517     4546   +29     
=======================================
+ Hits         4341     4369   +28     
- Misses        176      177    +1     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/EducationalTestingService/rsmtool/pull/683?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EducationalTestingService).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EducationalTestingService).

tamarl08: @desilinguist done! :)
desilinguist: Thank you so much for the careful review, @tamarl08! 🙇🏽 

Patch:
diff --git a/doc/conf.py b/doc/conf.py
index bd4cc8e9..c88e3065 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -51,6 +51,7 @@ intersphinx_mapping = {
     "python": ("https://docs.python.org/3", None),
     "skll": ("https://skll.readthedocs.io/en/latest", None),
     "pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None),
+    "prompt_toolkit": ("https://python-prompt-toolkit.readthedocs.io/en/stable/", None),
     "numpy": ("https://docs.scipy.org/doc/numpy/", None),
     "statsmodels": ("https://www.statsmodels.org/stable/", None),
 }
diff --git a/rsmtool/analyzer.py b/rsmtool/analyzer.py
index c8ad0e29..663c5ff0 100644
--- a/rsmtool/analyzer.py
+++ b/rsmtool/analyzer.py
@@ -11,7 +11,7 @@ Classes for analyzing RSMTool predictions, metrics, etc.
 import logging
 import warnings
 from functools import partial
-from typing import Dict, List, Optional, Tuple
+from typing import Dict, List, Optional, Tuple, Union
 
 import numpy as np
 import pandas as pd
@@ -19,6 +19,7 @@ from scipy.stats import kurtosis, pearsonr
 from sklearn.decomposition import PCA
 from sklearn.metrics import confusion_matrix, mean_squared_error, r2_score
 from skll.metrics import kappa
+from wandb.sdk.lib import RunDisabled
 from wandb.wandb_run import Run
 
 from .configuration_parser import Configuration
@@ -1700,7 +1701,7 @@ class Analyzer:
         self,
         data_container: DataContainer,
         configuration: Configuration,
-        wandb_run: Optional[Run] = None,
+        wandb_run: Union[Run, RunDisabled, None] = None,
     ) -> Tuple[Configuration, DataContainer]:
         """
         Run all analyses on the system scores (predictions).
@@ -1717,7 +1718,7 @@ class Analyzer:
             parameters (keys):  {"subgroups", "second_human_score_column",
             "use_scaled_predictions"}.
 
-        wandb_run : Optional[wandb.wandb_run.Run]
+        wandb_run : Union[wandb.wandb_run.Run, wandb.sdk.lib.RunDisabled, None]
             The wandb run object if wandb is enabled, ``None`` otherwise.
             If enabled, all the output data frames will be logged to this run
             as tables.
diff --git a/rsmtool/reporter.py b/rsmtool/reporter.py
index 811aa10e..338b3ffd 100644
--- a/rsmtool/reporter.py
+++ b/rsmtool/reporter.py
@@ -16,13 +16,14 @@ import os
 import sys
 import warnings
 from os.path import abspath, basename, dirname, join, normpath, splitext
-from typing import Dict, List, Optional
+from typing import Dict, List, Optional, Union
 
 import nbformat
 from nbconvert.exporters import HTMLExporter, NotebookExporter
 from nbconvert.exporters.templateexporter import default_filters
 from nbformat.warnings import DuplicateCellId, MissingIDFieldWarning
 from traitlets.config import Config
+from wandb.sdk.lib import RunDisabled
 from wandb.wandb_run import Run
 
 from rsmtool.configuration_parser import Configuration
@@ -122,7 +123,11 @@ notebook_path_dict = {
 class Reporter:
     """Class to generate Jupyter notebook reports and convert them to HTML."""
 
-    def __init__(self, logger: Optional[logging.Logger] = None, wandb_run: Optional[Run] = None):
+    def __init__(
+        self,
+        logger: Optional[logging.Logger] = None,
+        wandb_run: Union[Run, RunDisabled, None] = None,
+    ):
         """
         Initialize the Reporter object.
 
@@ -131,7 +136,7 @@ class Reporter:
         logger: Optional[logging.Logger]
             A Logger object. If ``None`` is passed, get logger from ``__name__``.
             Defaults to ``None``.
-        wandb_run: Optional[wandb.wandb_run.Run]
+        wandb_run: Union[wandb.wandb_run.Run, wandb.sdk.lib.RunDisabled, None]
             A wandb run object that will be used to log artifacts and tables.
             If ``None`` is passed, a new wandb run will be initialized if
             wandb is enabled in the configuration.
@@ -455,7 +460,7 @@ class Reporter:
             Type of the model. One of {``"BUILTIN"``, ``"SKLL"``, ``None``}. We allow
             ``None`` here so that rsmeval can use the same function.
             Defaults to ``None``.
-        context : str, optional
+        context : str
             Context of the tool in which we are validating. One of
             {``"rsmtool"``, ``"rsmeval"``, ``"rsmcompare"``}.
             Defaults to ``"rsmtool"``.
diff --git a/rsmtool/rsmeval.py b/rsmtool/rsmeval.py
index b0d57e65..2629ea88 100644
--- a/rsmtool/rsmeval.py
+++ b/rsmtool/rsmeval.py
@@ -17,6 +17,7 @@ from os.path import abspath, exists, join
 from pathlib import Path
 from typing import Any, Dict, List, Optional, Union
 
+from wandb.sdk.lib import RunDisabled
 from wandb.wandb_run import Run
 
 from .analyzer import Analyzer
@@ -36,7 +37,7 @@ def run_evaluation(
     output_dir: str,
     overwrite_output: bool = False,
     logger: Optional[logging.Logger] = None,
-    wandb_run: Optional[Run] = None,
+    wandb_run: Union[Run, RunDisabled, None] = None,
 ) -> None:
     """
     Run an rsmeval experiment using the given configuration.
@@ -64,7 +65,7 @@ def run_evaluation(
     logger : Optional[logging.Logger]
         A Logger object. If ``None`` is passed, get logger from ``__name__``.
         Defaults to ``None``.
-    wandb_run : Optional[wandb.wandb_run.Run]
+    wandb_run : Union[wandb.wandb_run.Run, wandb.sdk.lib.RunDisabled, None]
         A wandb run object that will be used to log artifacts and tables.
         If ``None`` is passed, a new wandb run will be initialized if
         wandb is enabled in the configuration.
diff --git a/rsmtool/rsmexplain.py b/rsmtool/rsmexplain.py
index b63e9768..0f17d005 100644
--- a/rsmtool/rsmexplain.py
+++ b/rsmtool/rsmexplain.py
@@ -25,6 +25,7 @@ import pandas as pd
 import shap
 from skll.data import FeatureSet
 from skll.learner import Learner
+from wandb.sdk.lib import RunDisabled
 from wandb.wandb_run import Run
 
 from .configuration_parser import Configuration, configure
@@ -146,7 +147,7 @@ def generate_explanation(
     output_dir: str,
     overwrite_output: bool = False,
     logger: Optional[logging.Logger] = None,
-    wandb_run: Optional[Run] = None,
+    wandb_run: Union[Run, RunDisabled, None] = None,
 ):
     """
     Generate a shap.Explanation object.
@@ -175,7 +176,7 @@ def generate_explanation(
     logger : Optional[logging.Logger]
         A Logger object. If ``None`` is passed, get logger from ``__name__``.
         Defaults to ``None``.
-    wandb_run : Optional[wandb.wandb_run.Run]
+    wandb_run : Union[wandb.wandb_run.Run, wandb.sdk.lib.RunDisabled, None]
         A wandb run object that will be used to log artifacts and tables.
         If ``None`` is passed, a new wandb run will be initialized if
         wandb is enabled in the configuration.
@@ -439,7 +440,7 @@ def generate_report(
     ids: Dict[int, str],
     configuration: Configuration,
     logger: Optional[logging.Logger] = None,
-    wandb_run: Optional[Run] = None,
+    wandb_run: Union[Run, RunDisabled, None] = None,
 ) -> None:
     """
     Generate an rsmexplain report.
@@ -462,7 +463,7 @@ def generate_report(
     logger : Optional[logging.Logger]
         A Logger object. If ``None`` is passed, get logger from ``__name__``.
         Defaults to ``None``.
-    wandb_run : Optional[wandb.wandb_run.Run]
+    wandb_run : Union[wandb.wandb_run.Run, wandb.sdk.lib.RunDisabled, None]
         A wandb run object that will be used to log artifacts and tables.
         If ``None`` is passed, a new wandb run will be initialized if
         wandb is enabled in the configuration. Defaults to ``None``.
diff --git a/rsmtool/rsmpredict.py b/rsmtool/rsmpredict.py
index 49fa726c..70d76bb7 100644
--- a/rsmtool/rsmpredict.py
+++ b/rsmtool/rsmpredict.py
@@ -19,6 +19,7 @@ from typing import Any, Dict, List, Optional, Union
 
 import numpy as np
 import pandas as pd
+from wandb.sdk.lib import RunDisabled
 from wandb.wandb_run import Run
 
 from .configuration_parser import Configuration, configure
@@ -315,7 +316,7 @@ def compute_and_save_predictions(
     output_file: str,
     feats_file: Optional[str] = None,
     logger: Optional[logging.Logger] = None,
-    wandb_run: Optional[Run] = None,
+    wandb_run: Union[Run, RunDisabled, None] = None,
 ) -> None:
     """
     Run rsmpredict using the given configuration.
@@ -344,7 +345,7 @@ def compute_and_save_predictions(
     logger : Optional[logging.Logger]
         A Logger object. If ``None`` is passed, get logger from ``__name__``.
         Defaults to ``None``.
-    wandb_run : Optional[wandb.wandb_run.Run]
+    wandb_run : Union[wandb.wandb_run.Run, wandb.sdk.lib.RunDisabled, None]
         A wandb run object that will be used to log artifacts and tables.
         If ``None`` is passed, a new wandb run will be initialized if
         wandb is enabled in the configuration.
diff --git a/rsmtool/rsmsummarize.py b/rsmtool/rsmsummarize.py
index 5d6dfd5b..dc4eaca5 100644
--- a/rsmtool/rsmsummarize.py
+++ b/rsmtool/rsmsummarize.py
@@ -18,6 +18,7 @@ from os.path import abspath, exists, join, normpath
 from pathlib import Path
 from typing import Any, Dict, Iterable, List, Optional, Union
 
+from wandb.sdk.lib import RunDisabled
 from wandb.wandb_run import Run
 
 from .configuration_parser import Configuration, configure
@@ -102,7 +103,7 @@ def run_summary(
     output_dir: str,
     overwrite_output: bool = False,
     logger: Optional[logging.Logger] = None,
-    wandb_run: Optional[Run] = None,
+    wandb_run: Union[Run, RunDisabled, None] = None,
 ) -> None:
     """
     Run rsmsummarize experiment using the given configuration.
@@ -132,7 +133,7 @@ def run_summary(
     logger : Optional[logging.Logger]
         A Logger object. If ``None`` is passed, get logger from ``__name__``.
         Defaults to ``None``.
-    wandb_run : Optional[wandb.wandb_run.Run]
+    wandb_run : Union[wandb.wandb_run.Run, wandb.sdk.lib.RunDisabled, None]
         A wandb run object that will be used to log artifacts and tables.
         If ``None`` is passed, a new wandb run will be initialized if
         wandb is enabled in the configuration.
diff --git a/rsmtool/rsmtool.py b/rsmtool/rsmtool.py
index 3d88cd46..58643da6 100644
--- a/rsmtool/rsmtool.py
+++ b/rsmtool/rsmtool.py
@@ -17,6 +17,7 @@ from os.path import abspath, exists, join
 from pathlib import Path
 from typing import Any, Dict, List, Optional, Union
 
+from wandb.sdk.lib import RunDisabled
 from wandb.wandb_run import Run
 
 from rsmtool.container import DatasetDict
@@ -39,7 +40,7 @@ def run_experiment(
     output_dir: str,
     overwrite_output: bool = False,
     logger: Optional[logging.Logger] = None,
-    wandb_run: Optional[Run] = None,
+    wandb_run: Union[Run, RunDisabled, None] = None,
 ) -> None:
     """
     Run an rsmtool experiment using the given configuration.
@@ -69,7 +70,7 @@ def run_experiment(
     logger : Optional[logging.Logger]
         A Logger object. If ``None`` is passed, get logger from ``__name__``.
         Defaults to ``None``.
-    wandb_run : Optional[wandb.wandb_run.Run]
+    wandb_run : Union[wandb.wandb_run.Run, wandb.sdk.lib.RunDisabled, None]
         A wandb run object that will be used to log artifacts and tables.
         If ``None`` is passed, a new wandb run will be initialized if
         wandb is enabled in the configuration.
diff --git a/rsmtool/utils/commandline.py b/rsmtool/utils/commandline.py
index 4352264e..2e38488d 100644
--- a/rsmtool/utils/commandline.py
+++ b/rsmtool/utils/commandline.py
@@ -16,6 +16,7 @@ import sys
 from collections import OrderedDict, namedtuple
 from itertools import chain, product
 from pathlib import Path
+from typing import Any, Dict, List, Optional, Union
 
 from prompt_toolkit.completion import FuzzyWordCompleter, PathCompleter, WordCompleter
 from prompt_toolkit.formatted_text import HTML
@@ -49,12 +50,12 @@ CmdOption = namedtuple(
 
 
 def setup_rsmcmd_parser(
-    name,
-    uses_output_directory=True,
-    allows_overwriting=False,
-    extra_run_options=[],
-    uses_subgroups=False,
-):
+    name: str,
+    uses_output_directory: bool = True,
+    allows_overwriting: bool = False,
+    extra_run_options: List[CmdOption] = [],
+    uses_subgroups: bool = False,
+) -> argparse.ArgumentParser:
     """
     Create argument parsers for RSM command-line utilities.
 
@@ -84,22 +85,22 @@ def setup_rsmcmd_parser(
     ----------
     name : str
         The name of the command-line tool for which we need the parser.
-    uses_output_directory : bool, optional
+    uses_output_directory : bool
         Add the ``output_dir`` positional argument to the "run" subcommand
         parser. This argument means that the respective tool uses an output
         directory to store its various outputs.
         Defaults to ``True``.
-    allows_overwriting : bool, optional
+    allows_overwriting : bool
         Add the ``-f``/``-force_write`` optional argument to the "run" subcommand
         parser. This argument allows the output for the respective
         tool to be overwritten even if it already exists (file) or contains
         output (directory).
         Defaults to ``False``.
-    extra_run_options : list, optional
+    extra_run_options : List[CmdOption]
         Any additional options to be added to the "run" subcommand parser,
         each specified as a ``CmdOption`` instance.
         Defaults to ``[]``.
-    uses_subgroups : bool, optional
+    uses_subgroups : bool
         Add the ``--subgroups`` optional argument to the "generate" subcommand
         parser. This argument means that the tool for which we are automatically
         generating a configuration file includes additional information when
@@ -257,7 +258,7 @@ def setup_rsmcmd_parser(
         # construct the arguments and keyword arguments needed for the
         # `add_argument()` call to the parser
         argparse_option_args = []
-        argparse_option_kwargs = {}
+        argparse_option_kwargs: Dict[str, Any] = {}
 
         # first add the destination and the help string
         argparse_option_kwargs["dest"] = f"{parser_option.dest}"
@@ -338,7 +339,7 @@ class InteractiveField:
         For example,
     """
 
-    def __init__(self, field_name, field_type, field_metadata):
+    def __init__(self, field_name: str, field_type: str, field_metadata: Dict[str, Any]):
         """
         Create a new InteractiveField instance.
 
@@ -348,31 +349,30 @@ class InteractiveField:
         Parameters
         ----------
         field_name : str
-            The internal name of the field as used in the configuration
-            dictionary.
+            The internal name of the field as used in the configuration dictionary.
         field_type : str
-            One of "required" or "optional", depending on whether the
+            One of ``"required"`` or ``"optional"``, depending on whether the
             field is required or optional in the configuration.
-        field_metadata : dict
+        field_metadata : Dict[str, Any]
             A dictionary containing the pre-defined metadata attributes
             for the given field. This dictionary is required to have
-            the "label" key and can have the following optional
-            keys: "choices", "count", and "type". For descriptions of what
-            these keys mean, see the docstring for the ``InteractiveField``
+            the ``"label"`` key and can have the following optional
+            keys: ``"choices"``, ``"count"``, and ``"type"``. For descriptions
+            of what these keys mean, see the docstring for the ``InteractiveField``
             class. Examples of such dictionaries can be found in
             ``rsmtool.utils.constants.INTERACTIVE_MODE_METADATA``.
 
         Raises
         ------
         ValueError
-            If the list of choices is not available for a field
-            of type "choice".
+            If the list of choices is not available for a field of type
+            ``"choice"``.
         """
         # assign metadata attributes to class attributes
         self.field_name = field_name
         self.field_type = field_type
         self.label = field_metadata["label"]
-        self.choices = field_metadata.get("choices", [])
+        self.choices: List[str] = field_metadata.get("choices", [])
         self.count = field_metadata.get("count", "single")
         self.data_type = field_metadata.get("type", "text")
 
@@ -411,7 +411,7 @@ class InteractiveField:
             allow_empty = field_type == "optional"
             self.validator = self._make_integer_validator(allow_empty=allow_empty)
 
-    def _make_boolean_validator(self, allow_empty=False):
+    def _make_boolean_validator(self, allow_empty: bool = False) -> Validator:
         """
         Create a validator for boolean fields.
 
@@ -420,10 +420,10 @@ class InteractiveField:
 
         Parameters
         ----------
-        allow_empty : bool, optional
+        allow_empty : bool
             If ``True``, it will allow the user to also just press
-            enter (i.e., input a blank string), in addition to "true"
-            or "false".
+            enter (i.e., input a blank string), in addition to ``"true"``
+            or ``"false"``.
             Defaults to ``False``.
 
         Returns
@@ -441,16 +441,16 @@ class InteractiveField:
         )
         return validator
 
-    def _make_choice_validator(self, choices):
+    def _make_choice_validator(self, choices: List[str]) -> Validator:
         """
         Create a validator for choice fields.
 
         This private method creates a validator for a field
-        with ``data_type`` of "choice".
+        with ``data_type`` of "choices".
 
         Parameters
         ----------
-        choices : list
+        choices : List[str]
             List of possible values for the field.
 
         Returns
@@ -464,7 +464,7 @@ class InteractiveField:
         )
         return validator
 
-    def _make_directory_completer(self):
+    def _make_directory_completer(self) -> PathCompleter:
         """
         Create a completer for directory fields.
 
@@ -479,7 +479,7 @@ class InteractiveField:
         """
         return PathCompleter(expanduser=False, only_directories=True)
 
-    def _make_directory_validator(self):
+    def _make_directory_validator(self) -> Validator:
         """
         Create a validator for directory fields.
 
@@ -497,7 +497,7 @@ class InteractiveField:
         )
         return validator
 
-    def _make_file_completer(self):
+    def _make_file_completer(self) -> PathCompleter:
         """
         Create a completer for file fields.
 
@@ -526,7 +526,7 @@ class InteractiveField:
 
         return PathCompleter(expanduser=False, file_filter=valid_file)
 
-    def _make_file_validator(self, allow_empty=False):
+    def _make_file_validator(self, allow_empty: bool = False) -> Validator:
         """
         Create a validator for file fields.
 
@@ -535,7 +535,7 @@ class InteractiveField:
 
         Parameters
         ----------
-        allow_empty : bool, optional
+        allow_empty : bool
             If ``True``, it will allow the user to also just press
             enter (i.e., input a blank string)
             Defaults to ``False``.
@@ -568,7 +568,7 @@ class InteractiveField:
         )
         return validator
 
-    def _make_file_format_validator(self):
+    def _make_file_format_validator(self) -> Validator:
         """
         Create a validator for file format fields.
 
@@ -590,7 +590,7 @@ class InteractiveField:
         )
         return validator
 
-    def _make_id_validator(self):
+    def _make_id_validator(self) -> Validator:
         """
         Create a validator for id fields.
 
@@ -611,7 +611,7 @@ class InteractiveField:
         )
         return validator
 
-    def _make_integer_validator(self, allow_empty=False):
+    def _make_integer_validator(self, allow_empty: bool = False) -> Validator:
         """
         Create a validator for integer fields.
 
@@ -620,7 +620,7 @@ class InteractiveField:
 
         Parameters
         ----------
-        allow_empty : bool, optional
+        allow_empty : bool
             If ``True``, it will allow the user to also just press
             enter (i.e., input a blank string)
             Defaults to ``False``.
@@ -642,7 +642,7 @@ class InteractiveField:
         )
         return validator
 
-    def _get_user_input(self):
+    def _get_user_input(self) -> Union[List[str], str]:
         """
         Display appropriate label and collect user input.
 
@@ -652,7 +652,7 @@ class InteractiveField:
 
         Returns
         -------
-        user_input : list or str
+        user_input : Union[List[str], str]
             A string for fields that accepts a single input
             or a list of strings for fields that accept multiple
             inputs, e.g., subgroups.
@@ -703,7 +703,7 @@ class InteractiveField:
 
         return user_input
 
-    def _finalize(self, user_input):
+    def _finalize(self, user_input: Union[List[str], str]) -> Any:
         """
         Convert given input to appropriate type.
 
@@ -712,29 +712,29 @@ class InteractiveField:
 
         Parameters
         ----------
-        user_input : list or str
-            Description
+        user_input : Union[List[str], str]
+            The user input for the field that is to be converted.
 
         Returns
         -------
-        final value
+        final value: Any
             The converted value.
         """
         if (user_input == "" or user_input == []) and self.field_type == "optional":
             final_value = DEFAULTS.get(self.field_name)
         else:
             # boolean fields need to be converted to actual booleans
-            if self.data_type == "boolean":
+            if isinstance(user_input, str) and self.data_type == "boolean":
                 final_value = False if user_input == "false" else True
             # and integer fields to integers/None
-            elif self.data_type == "integer":
+            elif isinstance(user_input, str) and self.data_type == "integer":
                 final_value = int(user_input)
             else:
                 final_value = user_input
 
         return final_value
 
-    def get_value(self):
+    def get_value(self) -> Union[bool, int, str, List[str]]:
         """
         Get value of instantiated interactive field.
 
@@ -742,9 +742,9 @@ class InteractiveField:
 
         Returns
         -------
-        final_value : list or str
-            The final value of the field which may be a list of
-            strings or a string.
+        final_value : Union[bool, int, str, List[str]]
+            The final value of the field which may be a string, a boolean,
+            an integer, or a list of strings.
         """
         # use a while loop to keep asking for the user input
         # until the user either enters it or uses ctrl-D
@@ -770,24 +770,33 @@ class ConfigurationGenerator:
     context : str
         Name of the command-line tool for which we are generating the
         configuration file.
-    as_string : bool, optional
+    as_string : bool
         If ``True``, return a formatted and indented string representation
         of the configuration, rather than a dictionary. Note that this only
         affects the batch-mode generation. Interactive generation always
         returns a string.
         Defaults to ``False``.
-    suppress_warnings : bool, optional
+    suppress_warnings : bool
         If ``True``, do not generate any warnings for batch-mode generation.
         Defaults to ``False``.
-    use_subgroups : bool, optional
+    use_subgroups : bool
         If ``True``, include subgroup-related sections in the list of general sections
         in the configuration file.
         Defaults to ``False``.
     """
 
     def __init__(
-        self, context, as_string=False, suppress_warnings=False, use_subgroups=False
-    ):  # noqa
+        self,
+        context: str,
+        as_string: bool = False,
+        suppress_warnings: bool = False,
+        use_subgroups: bool = False,
+    ):
+        """
+        Create a new ConfigurationGenerator instance.
+
+        See attributes above.
+        """
         self.context = context
         self.use_subgroups = use_subgroups
         self.suppress_warnings = suppress_warnings
@@ -803,11 +812,36 @@ class ConfigurationGenerator:
 
     def _convert_to_string(
         self,
-        config_object,
-        insert_url_comment=True,
-        insert_required_comment=True,
-        insert_optional_comment=True,
-    ):
+        config_object: Configuration,
+        insert_url_comment: bool = True,
+        insert_required_comment: bool = True,
+        insert_optional_comment: bool = True,
+    ) -> str:
+        """
+        Convert the given configuration object to a formatted string.
+
+        Parameters
+        ----------
+        config_object : Configuration
+            The configuration object to be converted to a string.
+        insert_url_comment : bool
+            If ``True``, insert a comment with the URL to the documentation
+            right above the first required field.
+            Defaults to ``True``.
+        insert_required_comment : bool
+            If ``True``, insert a comment right above the first required field
+            indicating that it is required.
+            Defaults to ``True``.
+        insert_optional_comment : bool
+            If ``True``, insert a comment right above the first optional field
+            indicating that it is optional.
+            Defaults to ``True``.
+
+        Returns
+        -------
+        configuration : str
+            The configuration object as a formatted string.
+        """
         configuration = str(config_object)
 
         # insert the URL comment first, right above the first required field
@@ -839,7 +873,7 @@ class ConfigurationGenerator:
 
         return configuration
 
-    def _get_all_general_section_names(self):
+    def _get_all_general_section_names(self) -> List[str]:
         default_general_sections_value = DEFAULTS.get("general_sections", "")
         default_custom_sections_value = DEFAULTS.get("custom_sections", "")
 
@@ -854,16 +888,18 @@ class ConfigurationGenerator:
             context=self.context,
         )
 
-    def interact(self, output_file_name=None):
+    def interact(self, output_file_name: Optional[str] = None) -> str:
         """
         Automatically generate an example configuration in interactive mode.
 
         Parameters
         ----------
-        output_file_name : str, optional
+        output_file_name : Optional[str]
             The file path where the configuration will eventually be saved.
             Note that this function just uses this name to inform the user.
-            The actual saving happens elsewhere.
+            The actual saving happens elsewhere. If ``None``, no message
+            about the output file is printed.
+            Defaults to ``None``.
 
         Returns
         -------
@@ -944,18 +980,19 @@ class ConfigurationGenerator:
         # a special wrapper method since we also want to insert comments
         return self._convert_to_string(config_object, insert_required_comment=False)
 
-    def generate(self):
+    def generate(self) -> Union[str, Dict[str, Any]]:
         """
         Automatically generate an example configuration in batch mode.
 
         Returns
         -------
-        configuration : dict or str
+        configuration : Union[str, Dict[str, Any]]
             The generated configuration either as a dictionary or
-            a formatted string, depending on the value of ``as_string``.
+            a formatted string, depending on the value of the ``as_string``
+            attribute.
         """
         # instantiate a dictionary that remembers key insertion order
-        configdict = OrderedDict()
+        configdict: Dict[str, Any] = OrderedDict()
 
         # insert the required fields first and give them a dummy value
         for required_field in self._required_fields:
@@ -977,6 +1014,7 @@ class ConfigurationGenerator:
 
         # if we were asked for string output, then convert this dictionary to
         # a string that will also insert some useful comments
+        configuration: Union[str, Dict[str, Any]]
         if self.as_string:
             configuration = self._convert_to_string(config_object)
         # otherwise we just return the dictionary underlying the Configuration object
diff --git a/rsmtool/utils/constants.py b/rsmtool/utils/constants.py
index caadc3dd..b3ad9e3a 100644
--- a/rsmtool/utils/constants.py
+++ b/rsmtool/utils/constants.py
@@ -9,10 +9,11 @@ Various RSMTool constants used across the codebase.
 """
 
 import re
+from typing import Any, Dict
 
 from .models import BUILTIN_MODELS, VALID_SKLL_MODELS
 
-DEFAULTS = {
+DEFAULTS: Dict[str, Any] = {
     "id_column": "spkitemid",
     "description": "",
     "description_old": "",
@@ -316,7 +317,7 @@ CONFIGURATION_DOCUMENTATION_SLUGS = {
 
 VALID_PARSER_SUBCOMMANDS = ["generate", "run"]
 
-INTERACTIVE_MODE_METADATA = {
+INTERACTIVE_MODE_METADATA: Dict[str, Dict[str, Any]] = {
     "experiment_id": {"label": "Experiment ID", "type": "id"},
     "comparison_id": {"label": "Comparison ID", "type": "id"},
     "summary_id": {"label": "Summary ID", "type": "id"},
diff --git a/rsmtool/utils/conversion.py b/rsmtool/utils/conversion.py
index 07f28a36..6abf08c1 100644
--- a/rsmtool/utils/conversion.py
+++ b/rsmtool/utils/conversion.py
@@ -8,36 +8,38 @@ Utility classes and functions for type conversion.
 :organization: ETS
 """
 
+from typing import Any, Tuple
+
 from skll.data import safe_float as string_to_number
 
 from .constants import RSMEXPLAIN_RANGE_REGEXP
 
 
-def int_to_float(value):
+def int_to_float(value: Any) -> Any:
     """
     Convert integer to float, if possible.
 
     Parameters
     ----------
-    value
-        Name of the value we want to convert.
+    value: Any
+        The value we want to convert.
 
     Returns
     -------
-    value
+    value: Any
         Value converted to float, if possible
     """
-    return float(value) if type(value) == int else value
+    return float(value) if isinstance(value, int) else value
 
 
-def convert_to_float(value):
+def convert_to_float(value: Any) -> Any:
     """
     Convert value to float, if possible.
 
     Parameters
     ----------
     value
-        Name of the experiment file we want to locate.
+        The value we want to convert
 
     Returns
     -------
@@ -47,7 +49,7 @@ def convert_to_float(value):
     return int_to_float(string_to_number(value))
 
 
-def parse_range(value):
+def parse_range(value: str) -> Tuple[int, int]:
     """
     Parse range strings used in rsmexplain.
 
@@ -62,6 +64,11 @@ def parse_range(value):
     -------
     Tuple[int, int]
         A tuple containing two integers.
+
+    Raises
+    ------
+    ValueError
+        If the range string is not valid.
     """
     valid_range = False
     if matchobj := RSMEXPLAIN_RANGE_REGEXP.match(value):
@@ -81,4 +88,5 @@ def parse_range(value):
     if not valid_range:
         raise ValueError(f"invalid value '{value}' specified for range")
     else:
-        return [start_index, end_index]
+        assert isinstance(start_index, int) and isinstance(end_index, int)
+        return (start_index, end_index)
diff --git a/rsmtool/utils/cross_validation.py b/rsmtool/utils/cross_validation.py
index bba97828..2a627b9c 100644
--- a/rsmtool/utils/cross_validation.py
+++ b/rsmtool/utils/cross_validation.py
@@ -9,30 +9,36 @@ Various utility functions used for cross-validation.
 import logging
 from pathlib import Path
 from shutil import copyfile
+from typing import Optional, Tuple
 
-from pandas import concat
+from pandas import DataFrame, concat
 from sklearn.model_selection import KFold, LeaveOneGroupOut
 from skll.config.utils import load_cv_folds
 
+from rsmtool.configuration_parser import Configuration
 from rsmtool.reader import DataReader
 from rsmtool.rsmtool import run_experiment
 from rsmtool.utils.logging import get_file_logger
 from rsmtool.writer import DataWriter
 
 
-def create_xval_files(configuration, output_dir, logger=None):
+def create_xval_files(
+    configuration: Configuration, output_dir: str, logger: Optional[logging.Logger] = None
+) -> Tuple[DataFrame, int]:
     """
     Create all files needed for the cross-validation experiment.
 
     Parameters
     ----------
-    configuration : rsmtool.configuration_parser.Configuration
+    configuration : Configuration
         A Configuration object holding the user-specified configuration
-        for "rsmxval".
+        for rsmxval.
     output_dir : str
-        Path to the output directory specified for "rsmxval".
-    logger : None, optional
-        Logger object used to log messages from this function.
+        Path to the output directory specified for rsmxval.
+    logger : Optional[logging.Logger]
+        Logger object used to log messages from this function. If ``None``,
+        a new logger is created.
+        Defaults to ``None``.
 
     Returns
     -------
@@ -186,7 +192,7 @@ def create_xval_files(configuration, output_dir, logger=None):
     return df_train, folds
 
 
-def process_fold(fold_num, foldsdir):
+def process_fold(fold_num: int, foldsdir: str) -> None:
     """
     Run RSMTool on the specified numbered fold.
 
@@ -206,10 +212,10 @@ def process_fold(fold_num, foldsdir):
 
     # run RSMTool on the given fold and have it log its output to the file
     config_file = fold_dir / "rsmtool.json"
-    run_experiment(config_file, fold_dir, False, logger=logger)
+    run_experiment(config_file, str(fold_dir), False, logger=logger)
 
 
-def combine_fold_prediction_files(foldsdir, file_format="csv"):
+def combine_fold_prediction_files(foldsdir: str, file_format: str = "csv") -> DataFrame:
     """
     Combine predictions from all folds into a single data frame.
 
@@ -219,8 +225,8 @@ def combine_fold_prediction_files(foldsdir, file_format="csv"):
         The directory which stores the output for each fold experiment.
     file_format : str
         The file format (extension) for the file to be written to disk.
-        One of {"csv", "xlsx", "tsv"}.
-        Defaults to "csv".
+        One of {``"csv"``, ``"xlsx"``, ``"tsv"``}.
+        Defaults to ``"csv"``.
 
     Returns
     -------
@@ -233,7 +239,7 @@ def combine_fold_prediction_files(foldsdir, file_format="csv"):
     # iterate over each fold in the given directory & read and save predictions
     for prediction_file in Path(foldsdir).glob(f"**/*pred_processed*.{file_format}"):
         df_fold_predictions = DataReader.read_from_file(
-            prediction_file, converters={"spkitemid": str}
+            str(prediction_file), converters={"spkitemid": str}
         )
         prediction_dfs.append(df_fold_predictions)
 
diff --git a/rsmtool/utils/files.py b/rsmtool/utils/files.py
index 92b76036..89d871ea 100644
--- a/rsmtool/utils/files.py
+++ b/rsmtool/utils/files.py
@@ -13,11 +13,12 @@ import re
 from glob import glob
 from os.path import join
 from pathlib import Path
+from typing import Any, Dict, Union
 
 from .constants import POSSIBLE_EXTENSIONS
 
 
-def parse_json_with_comments(pathlike):
+def parse_json_with_comments(pathlike: Union[str, Path]) -> Dict[str, Any]:
     """
     Parse a JSON file after removing any comments.
 
@@ -27,13 +28,13 @@ def parse_json_with_comments(pathlike):
 
     Parameters
     ----------
-    filename : str or os.PathLike
+    filename : Union[str, Path]
         Path to the input JSON file either as a string
         or as a ``pathlib.Path`` object.
 
     Returns
     -------
-    obj : dict
+    obj : Dict[str, Any]
         JSON object representing the input file.
 
     Note
@@ -67,7 +68,7 @@ def parse_json_with_comments(pathlike):
         return config
 
 
-def has_files_with_extension(directory, ext):
+def has_files_with_extension(directory: str, ext: str) -> bool:
     """
     Check if the directory has any files with the given extension.
 
@@ -88,7 +89,7 @@ def has_files_with_extension(directory, ext):
     return len(files_with_extension) > 0
 
 
-def get_output_directory_extension(directory, experiment_id):
+def get_output_directory_extension(directory: str, experiment_id: str) -> str:
     """
     Check output directory to determine what file extensions exist.
 
@@ -110,14 +111,14 @@ def get_output_directory_extension(directory, experiment_id):
     Returns
     -------
     extension : str
-        The extension that output files in this directory
-        end with. One of {"csv", "tsv", "xlsx"}.
+        The extension that output files in this directory end with. One of
+        {``"csv"``, ``"tsv"``, ``"xlsx"``}.
 
     Raises
     ------
     ValueError
-        If any files in the directory have extensions
-        other than "csv", "tsv", or "xlsx".
+        If any files in the directory have extensions other than ``"csv"``,
+        ``"tsv"``, or ``"xlsx"``.
     """
     extension = "csv"
     extensions_identified = {
diff --git a/rsmtool/utils/logging.py b/rsmtool/utils/logging.py
index 94117fce..693c5c76 100644
--- a/rsmtool/utils/logging.py
+++ b/rsmtool/utils/logging.py
@@ -29,10 +29,18 @@ class LogFormatter(logging.Formatter):
     err_fmt = "ERROR: %(msg)s"
     dbg_fmt = "DEBUG: %(module)s: %(lineno)d: %(msg)s"
 
-    def __init__(self, fmt="%(levelno)s: %(msg)s"):  # noqa: D107
+    def __init__(self, fmt: str = "%(levelno)s: %(msg)s"):
+        """
+        Initialize the formatter.
+
+        Parameters
+        ----------
+        fmt : str
+            The format string to use for formatting the log record.
+        """
         logging.Formatter.__init__(self, fmt)
 
-    def format(self, record):
+    def format(self, record: logging.LogRecord) -> str:
         """
         Format the given record.
 
@@ -40,6 +48,11 @@ class LogFormatter(logging.Formatter):
         ----------
         record : logging.LogRecord
             The record to format.
+
+        Returns
+        -------
+        str
+            The formatted log record.
         """
         # Save the original format configured by the user
         # when the logger formatter was instantiated
@@ -71,7 +84,7 @@ class LogFormatter(logging.Formatter):
         return result
 
 
-def get_file_logger(logger_name, log_file_path):
+def get_file_logger(logger_name: str, log_file_path: str) -> logging.Logger:
     """
     Create and return a file-based logger.
 
@@ -88,7 +101,7 @@ def get_file_logger(logger_name, log_file_path):
 
     Returns
     -------
-    logger
+    logging.Logger
         A logging.Logger object attached to a file handler.
     """
     logger = logging.getLogger(logger_name)
diff --git a/rsmtool/utils/metrics.py b/rsmtool/utils/metrics.py
index 47142b43..72c78317 100644
--- a/rsmtool/utils/metrics.py
+++ b/rsmtool/utils/metrics.py
@@ -9,12 +9,17 @@ Utility functions for computing various RSMTool metrics.
 """
 
 import warnings
+from typing import List, Optional
 
 import numpy as np
 import pandas as pd
+from skll.data import FeatureSet
+from skll.learner import Learner
 
 
-def compute_expected_scores_from_model(model, featureset, min_score, max_score):
+def compute_expected_scores_from_model(
+    model: Learner, featureset: FeatureSet, min_score: float, max_score: float
+) -> np.ndarray:
     """
     Compute expected scores using probability distributions over labels.
 
@@ -33,7 +38,7 @@ def compute_expected_scores_from_model(model, featureset, min_score, max_score):
 
     Returns
     -------
-    expected_scores: np.array
+    expected_scores: numpy.ndarray
         A numpy array containing the expected scores.
 
     Raises
@@ -45,6 +50,8 @@ def compute_expected_scores_from_model(model, featureset, min_score, max_score):
         does not match what the model predicts in its probability
         distribution.
     """
+    max_score = int(max_score)
+    min_score = int(min_score)
     if hasattr(model.model, "predict_proba"):
         # Tell the model we want probabiltiies as output. This is likely already set
         # to True but it might not be, e.g., when using rsmpredict.
@@ -75,18 +82,18 @@ def compute_expected_scores_from_model(model, featureset, min_score, max_score):
     return expected_scores
 
 
-def covariance_to_correlation(m):
+def covariance_to_correlation(cov: np.ndarray) -> np.ndarray:
     """
     Implement the R ``cov2cor`` function in Python.
 
     Parameters
     ----------
-    m : np.array
+    cov : numpy.ndarray
         The covariance matrix.
 
     Returns
     -------
-    retval : np.array
+    retval : numpy.ndarray
         The cross-correlation matrix.
 
     Raises
@@ -95,17 +102,17 @@ def covariance_to_correlation(m):
         If the input matrix is not square.
     """
     # make sure the matrix is square
-    numrows, numcols = m.shape
+    numrows, numcols = cov.shape
     if not numrows == numcols:
         raise ValueError("Input matrix must be square")
 
-    Is = np.sqrt(1 / np.diag(m))
-    retval = Is * m * np.repeat(Is, numrows).reshape(numrows, numrows)
+    Is = np.sqrt(1 / np.diag(cov))
+    retval = Is * cov * np.repeat(Is, numrows).reshape(numrows, numrows)
     np.fill_diagonal(retval, 1.0)
     return retval
 
 
-def partial_correlations(df):
+def partial_correlations(df: pd.DataFrame) -> pd.DataFrame:
     """
     Implement the R ``pcor`` function from ``ppcor`` package in Python.
 
@@ -114,12 +121,12 @@ def partial_correlations(df):
 
     Parameters
     ----------
-    df : pd.DataFrame
+    df : pandas.DataFrame
         Data frame containing the feature values.
 
     Returns
     -------
-    df_pcor : pd.DataFrame
+    df_pcor : pandas.DataFrame
         Data frame containing the partial correlations of of each
         pair of variables in the given data frame ``df``,
         excluding all other variables.
@@ -165,17 +172,17 @@ def partial_correlations(df):
     return df_pcor
 
 
-def agreement(score1, score2, tolerance=0):
+def agreement(score1: List[int], score2: List[int], tolerance: int = 0) -> float:
     """
     Compute the agreement between two raters, under given tolerance.
 
     Parameters
     ----------
-    score1 : list of int
+    score1 : List[int]
         List of rater 1 scores
-    score2 : list of int
+    score2 : List[int]
         List of rater 2 scores
-    tolerance : int, optional
+    tolerance : int
         Difference in scores that is acceptable.
         Defaults to 0.
 
@@ -195,13 +202,13 @@ def agreement(score1, score2, tolerance=0):
 
 
 def standardized_mean_difference(
-    y_true_observed,
-    y_pred,
-    population_y_true_observed_sd=None,
-    population_y_pred_sd=None,
-    method="unpooled",
-    ddof=1,
-):
+    y_true_observed: np.ndarray,
+    y_pred: np.ndarray,
+    population_y_true_observed_sd: Optional[float] = None,
+    population_y_pred_sd: Optional[float] = None,
+    method: str = "unpooled",
+    ddof: int = 1,
+) -> float:
     """
     Compute the standardized mean difference between system and human scores.
 
@@ -210,40 +217,36 @@ def standardized_mean_difference(
 
     Parameters
     ----------
-    y_true_observed : array-like
+    y_true_observed : numpy.ndarray
         The observed scores for the group or subgroup.
-    y_pred : array-like
-        The predicted score for the group or subgroup.
-    population_y_true_observed_sd : float, optional
-        The population true score standard deviation.
-        When the SMD is being calculated for a subgroup,
-        this should be the standard deviation for the whole
-        population.
+    y_pred : numpy.ndarray
+        The predicted scores for the group or subgroup.
+    population_y_true_observed_sd : Optional[float]
+        The population true score standard deviation. When the SMD is being
+        calculated for a subgroup, this should be the standard deviation for
+        the whole population.
         Defaults to ``None``.
-    population_y_pred_sd : float, optional
-        The predicted score standard deviation.
-        When the SMD is being calculated for a subgroup,
-        this should be the standard deviation for the whole
-        population.
+    population_y_pred_sd : Optional[float]
+        The predicted score standard deviation. When the SMD is being calculated
+        for a subgroup, this should be the standard deviation for the whole population.
         Defaults to ``None``.
-    method : str, optional
+    method : str
         The SMD method to use. Possible options are:
 
-        - "williamson": Denominator is the pooled population standard deviation
+        - ``"williamson"``: Denominator is the pooled population standard deviation
           of ``y_true_observed`` and ``y_pred`` computed using
           ``population_y_true_observed_sd`` and ``population_y_pred_sd``.
-        - "johnson": Denominator is ``population_y_true_observed_sd``.
+        - ``"johnson"``: Denominator is ``population_y_true_observed_sd``.
         - "pooled": Denominator is the pooled standard deviation of
           ``y_true_observed`` and ``y_pred`` for this group.
-        - "unpooled": Denominator is the standard deviation of
+        - ``"unpooled"``: Denominator is the standard deviation of
           ``y_true_observed`` for this group.
 
-        Defaults to "unpooled".
+        Defaults to ``"unpooled"``.
 
-    ddof : int, optional
-        The delta degrees of freedom. The divisor used in
-        calculations is N - ddof, where N represents the
-        number of elements.
+    ddof : int
+        The delta degrees of freedom. The divisor used in calculations is N - ddof,
+        where N represents the number of elements.
         Defaults to 1.
 
     Returns
@@ -302,14 +305,14 @@ def standardized_mean_difference(
 
 
 def difference_of_standardized_means(
-    y_true_observed,
-    y_pred,
-    population_y_true_observed_mn=None,
-    population_y_pred_mn=None,
-    population_y_true_observed_sd=None,
-    population_y_pred_sd=None,
-    ddof=1,
-):
+    y_true_observed: np.ndarray,
+    y_pred: np.ndarray,
+    population_y_true_observed_mn: Optional[float] = None,
+    population_y_pred_mn: Optional[float] = None,
+    population_y_true_observed_sd: Optional[float] = None,
+    population_y_pred_sd: Optional[float] = None,
+    ddof: int = 1,
+) -> Optional[float]:
     """
     Calculate the difference between standardized means.
 
@@ -319,45 +322,36 @@ def difference_of_standardized_means(
 
     Parameters
     ----------
-    y_true_observed : array-like
+    y_true_observed : numpy.ndarray
         The observed scores for the group or subgroup.
-    y_pred : array-like
-        The predicted score for the group or subgroup.
-        The predicted scores.
-    population_y_true_observed_mn : float, optional
-        The population true score mean.
-        When the DSM is being calculated for a subgroup,
-        this should be the mean for the whole
-        population.
+    y_pred : numpy.ndarray
+        The predicted scores for the group or subgroup.
+    population_y_true_observed_mn : Optional[float]
+        The population true score mean. When the DSM is being calculated for a
+        subgroup, this should be the mean for the whole population.
         Defaults to ``None``.
-    population_y_pred_mn : float, optional
-        The predicted score mean.
-        When the DSM is being calculated for a subgroup,
-        this should be the mean for the whole
-        population.
+    population_y_pred_mn : Optional[float]
+        The predicted score mean. When the DSM is being calculated for a subgroup,
+        this should be the mean for the whole population.
         Defaults to ``None``.
-    population_y_true_observed_sd : float, optional
-        The population true score standard deviation.
-        When the DSM is being calculated for a subgroup,
-        this should be the standard deviation for the whole
-        population.
+    population_y_true_observed_sd : Optional[float]
+        The population true score standard deviation. When the DSM is being
+        calculated for a subgroup, this should be the standard deviation for the
+        whole population.
         Defaults to ``None``.
-    population_y_pred_sd : float, optional
-        The predicted score standard deviation.
-        When the DSM is being calculated for a subgroup,
-        this should be the standard deviation for the whole
-        population.
-        Defaults to None.
-    ddof : int, optional
-        The delta degrees of freedom. The divisor used in
-        calculations is N - ddof, where N represents the
-        number of elements.
+    population_y_pred_sd : Optional[float]
+        The predicted score standard deviation. When the DSM is being calculated
+        for a subgroup, this should be the standard deviation for the whole population.
+        Defaults to ``None``.
+    ddof : int
+        The delta degrees of freedom. The divisor used in calculations is N - ddof
+        where N represents the number of elements.
         Defaults to 1.
 
     Returns
     -------
-    difference_of_std_means : array-like
-        The difference of standardized means
+    difference_of_std_means : Optional[float]
+        The difference of standardized means.
 
     Raises
     ------
@@ -438,7 +432,9 @@ def difference_of_standardized_means(
     return difference_of_std_means
 
 
-def quadratic_weighted_kappa(y_true_observed, y_pred, ddof=0):  # noqa: D301
+def quadratic_weighted_kappa(
+    y_true_observed: np.ndarray, y_pred: np.ndarray, ddof: int = 0
+) -> float:
     r"""
     Calculate quadratic-weighted kappa for both discrete and continuous values.
 
@@ -462,11 +458,11 @@ def quadratic_weighted_kappa(y_true_observed, y_pred, ddof=0):  # noqa: D301
 
     Parameters
     ----------
-    y_true_observed : array-like
+    y_true_observed : numpy.ndarray
         The observed scores.
-    y_pred : array-like
+    y_pred : numpy.ndarray
         The predicted scores.
-    ddof : int, optional
+    ddof : int
         Means Delta Degrees of Freedom. The divisor used in
         calculations is N - ddof, where N represents the
         number of elements. When ddof is set to zero, the results
diff --git a/rsmtool/utils/models.py b/rsmtool/utils/models.py
index 924c5cde..895ca5ba 100644
--- a/rsmtool/utils/models.py
+++ b/rsmtool/utils/models.py
@@ -42,7 +42,7 @@ VALID_SKLL_MODELS = [
 ]
 
 
-def is_skll_model(model_name):
+def is_skll_model(model_name: str) -> bool:
     """
     Check whether the given model is a valid learner name in SKLL.
 
@@ -63,7 +63,7 @@ def is_skll_model(model_name):
     return hasattr(_skll_module, model_name) and model_name != "LinearRegression"
 
 
-def is_built_in_model(model_name):
+def is_built_in_model(model_name: str) -> bool:
     """
     Check whether the given model is a valid built-in model.
 
diff --git a/rsmtool/utils/notebook.py b/rsmtool/utils/notebook.py
index e3106890..1b98b832 100644
--- a/rsmtool/utils/notebook.py
+++ b/rsmtool/utils/notebook.py
@@ -14,6 +14,7 @@ from os.path import exists, isabs, relpath
 from pathlib import Path
 from string import Template
 from textwrap import wrap
+from typing import Dict, List, Optional, Tuple, Union
 
 from IPython.display import HTML, display
 
@@ -29,7 +30,7 @@ INTERMEDIATE_TABLE_ROW_STRING = """
 """
 
 
-def float_format_func(num, prec=3, scientific=False):
+def float_format_func(num: float, prec: int = 3, scientific: bool = False) -> str:
     """
     Format given float to the specified precision as a string.
 
@@ -37,12 +38,12 @@ def float_format_func(num, prec=3, scientific=False):
     ----------
     num : float
         The floating point number to format.
-    prec: int, optional
+    prec: int
         The number of decimal places to use when displaying the number.
         Defaults to 3.
-    scientific: bool, optional
-        Whether to display the number in scientific notiation if
-        the rounded version is "0.000".
+    scientific: bool
+        Whether to display the number in scientific notiation if the rounded
+        version is "0.000".
         Defaults to ``False``.
 
     Returns
@@ -58,7 +59,7 @@ def float_format_func(num, prec=3, scientific=False):
     return ans
 
 
-def int_or_float_format_func(num, prec=3):
+def int_or_float_format_func(num: Union[int, float], prec: int = 3) -> str:
     """
     Identify whether the number is float or integer.
 
@@ -67,9 +68,9 @@ def int_or_float_format_func(num, prec=3):
 
     Parameters
     ----------
-    num : float or int
+    num : Union[int, float]
         The number to format and display.
-    prec : int, optional
+    prec : int
         The number of decimal places to display if x is a float.
         Defaults to 3.
 
@@ -85,7 +86,14 @@ def int_or_float_format_func(num, prec=3):
     return ans
 
 
-def custom_highlighter(num, low=0, high=1, prec=3, absolute=False, span_class="bold"):
+def custom_highlighter(
+    num: float,
+    low: float = 0,
+    high: float = 1,
+    prec: int = 3,
+    absolute: bool = False,
+    span_class: str = "bold",
+) -> str:
     """
     Convert float to an HTML <span> element with given class.
 
@@ -109,8 +117,9 @@ def custom_highlighter(num, low=0, high=1, prec=3, absolute=False, span_class="b
         If ``True``, use the absolute value of x for comparison.
         Defaults to ``False``.
     span_class : str
-        One of "bold" or "color". These are the two classes
-        available for the HTML span tag.
+        One of ``"bold"`` or ``"color"``. These are the two classes available
+        for the HTML span tag.
+        Defaults to ``"bold"``.
 
     Returns
     -------
@@ -127,7 +136,9 @@ def custom_highlighter(num, low=0, high=1, prec=3, absolute=False, span_class="b
     return ans
 
 
-def bold_highlighter(num, low=0, high=1, prec=3, absolute=False):
+def bold_highlighter(
+    num: float, low: float = 0, high: float = 1, prec: int = 3, absolute: bool = False
+) -> str:
     """
     Instantiate a ``custom_highlighter()`` with "bold" as default class.
 
@@ -157,7 +168,9 @@ def bold_highlighter(num, low=0, high=1, prec=3, absolute=False):
     return ans
 
 
-def color_highlighter(num, low=0, high=1, prec=3, absolute=False):
+def color_highlighter(
+    num: float, low: float = 0, high: float = 1, prec: int = 3, absolute: bool = False
+) -> str:
     """
     Instantiate a ``custom_highlighter()`` with "color" as the default class.
 
@@ -187,7 +200,9 @@ def color_highlighter(num, low=0, high=1, prec=3, absolute=False):
     return ans
 
 
-def compute_subgroup_plot_params(group_names, num_plots):
+def compute_subgroup_plot_params(
+    group_names: List[str], num_plots: int
+) -> Tuple[int, int, int, int, List[str]]:
     """
     Compute subgroup plot and figure parameters.
 
@@ -196,7 +211,7 @@ def compute_subgroup_plot_params(group_names, num_plots):
 
     Parameters
     ----------
-    group_names : list of str
+    group_names : List[str]
         A list of subgroup names for plots.
     num_plots : int
         The number of plots to compute.
@@ -211,7 +226,7 @@ def compute_subgroup_plot_params(group_names, num_plots):
         The number of rows for the plots.
     num_columns : int
         The number of columns for the plots.
-    wrapped_group_names : list of str
+    wrapped_group_names : List[str]
         A list of group names for plots.
     """
     wrapped_group_names = ["\n".join(wrap(str(gn), 20)) for gn in group_names]
@@ -231,7 +246,9 @@ def compute_subgroup_plot_params(group_names, num_plots):
     return (figure_width, figure_height, num_rows, num_columns, wrapped_group_names)
 
 
-def get_thumbnail_as_html(path_to_image, image_id, path_to_thumbnail=None):
+def get_thumbnail_as_html(
+    path_to_image: str, image_id: int, path_to_thumbnail: Optional[str] = None
+) -> str:
     """
     Generate HTML for a clickable thumbnail of given image.
 
@@ -243,15 +260,13 @@ def get_thumbnail_as_html(path_to_image, image_id, path_to_thumbnail=None):
     Parameters
     ----------
     path_to_image : str
-        The absolute or relative path to the image.
-        If an absolute path is provided, it will be
-        converted to a relative path.
+        The absolute or relative path to the image. If an absolute path is
+        provided, it will be converted to a relative path.
     image_id : int
-        The id of the <img> tag in the HTML. This must
-        be unique for each <img> tag.
-    path_to_thumbnail : str, optional
-        If you would like to use a different thumbnail
-        image, specify the path to this thumbnail.
+        The id of the <img> tag in the HTML. This must be unique for each <img> tag.
+    path_to_thumbnail : Optional[str]
+        If you would like to use a different thumbnail image, specify the path
+        to this thumbnail.
         Defaults to ``None``.
 
     Returns
@@ -317,7 +332,9 @@ def get_thumbnail_as_html(path_to_image, image_id, path_to_thumbnail=None):
     return image
 
 
-def show_thumbnail(path_to_image, image_id, path_to_thumbnail=None):
+def show_thumbnail(
+    path_to_image: str, image_id: int, path_to_thumbnail: Optional[str] = None
+) -> None:
     """
     Display the HTML for an image thumbnail in a Jupyter notebook.
 
@@ -327,26 +344,21 @@ def show_thumbnail(path_to_image, image_id, path_to_thumbnail=None):
     Parameters
     ----------
     path_to_image : str
-        The absolute or relative path to the image.
-        If an absolute path is provided, it will be
-        converted to a relative path.
+        The absolute or relative path to the image. If an absolute path is
+        provided, it will be converted to a relative path.
     image_id : int
-        The id of the <img> tag in the HTML. This must
-        be unique for each <img> tag.
-    path_to_thumbnail : str, optional
-        If you would like to use a different thumbnail
-        image, specify the path to the thumbnail.
+        The id of the <img> tag in the HTML. This must be unique for each <img> tag.
+    path_to_thumbnail : Optional[str]
+        If you would like to use a different thumbnail image, specify the path
+        to the thumbnail.
         Defaults to ``None``.
-
-    Displays
-    --------
-    display : IPython.core.display.HTML
-        The HTML for the thumbnail image.
     """
     display(HTML(get_thumbnail_as_html(path_to_image, image_id, path_to_thumbnail)))
 
 
-def get_files_as_html(output_dir, experiment_id, file_format, replace_dict={}):
+def get_files_as_html(
+    output_dir: str, experiment_id: str, file_format: str, replace_dict: Dict[str, str] = {}
+) -> str:
     """
     Generate an HTML list for each output file in given directory.
 
@@ -361,8 +373,8 @@ def get_files_as_html(output_dir, experiment_id, file_format, replace_dict={}):
         The experiment ID.
     file_format : str
         The format of the output files.
-    replace_dict : dict, optional
-        A dictionary which makes file names to descriptions.
+    replace_dict : Dict[str, str]
+        A dictionary which maps file names to descriptions.
         Defaults to ``{}``.
 
     Returns
@@ -370,11 +382,11 @@ def get_files_as_html(output_dir, experiment_id, file_format, replace_dict={}):
     html_string : str
         HTML string with file descriptions and links.
     """
-    output_dir = Path(output_dir)
-    parent_dir = output_dir.parent
+    output_path = Path(output_dir)
+    parent_dir = output_path.parent
 
     # get the list of intermediate files generated for this experiment
-    files = list(output_dir.glob(f"*.{file_format}"))
+    files = list(output_path.glob(f"*.{file_format}"))
 
     # get the bare filenames for each file since we will need those for sorting
     filenames = [file.stem.replace(f"{experiment_id}_", "") for file in files]
@@ -426,25 +438,20 @@ def get_files_as_html(output_dir, experiment_id, file_format, replace_dict={}):
     return f"<table>{table_rows}</table>"
 
 
-def show_files(context, output_dir, experiment_id, file_format):
+def show_files(context: str, output_dir: str, experiment_id: str, file_format: str) -> None:
     """
     Show files for given context and directory as a table in a Jupyter notebook.
 
     Parameters
     ----------
     context: str
-        The tool context: one of {"rsmtool", "rsmeval", "rsmsummarize"}.
+        The tool context: one of {``"rsmtool"``, ``"rsmeval"``, ``"rsmsummarize"``}.
     output_dir : str
         The output directory.
     experiment_id : str
         The experiment ID.
     file_format : str
         The format of the output files.
-
-    Displays
-    --------
-    display : IPython.core.display.HTML
-        The HTML file descriptions and links.
     """
     html_string = get_files_as_html(
         output_dir,
diff --git a/rsmtool/utils/prmse.py b/rsmtool/utils/prmse.py
index 54f7c470..5ee69153 100644
--- a/rsmtool/utils/prmse.py
+++ b/rsmtool/utils/prmse.py
@@ -13,42 +13,45 @@ The derivations and formulas were provided by Matt Johnson.
 """
 
 import warnings
+from typing import List, Optional, Union
 
 import numpy as np
 import pandas as pd
 
 
-def get_n_human_scores(human_scores):
+def get_n_human_scores(human_scores: np.ndarray) -> np.ndarray:
     """
     Get the number of available human scores for each response.
 
     Parameters
     ----------
-    human_scores : array-like of shape (n_samples, n_ratings)
-        Human ratings for each response.
+    human_scores : numpy.ndarray
+        Human ratings for each response of shape (n_samples, n_ratings).
 
     Returns
     -------
-    n_scores : array-like of shape (n_samples, )
-        Total number of not None human scores
+    n_scores : numpy.ndarray
+        Total number of human scores of shape (n_samples, ). Only includes
+        scores that are not NaN.
     """
     n_scores = (~np.isnan(human_scores)).sum(axis=1)
     return n_scores
 
 
-def variance_of_errors(human_scores):
+def variance_of_errors(human_scores: np.ndarray) -> Optional[float]:
     """
     Estimate the variance of errors in human scores.
 
     Parameters
     ----------
-    human_scores : array-like of shape (n_samples, n_ratings)
-        Human ratings for each response.
+    human_scores : numpy.ndarray
+        Human ratings for each response of shape (n_samples, n_ratings).
 
     Returns
     -------
-    variance_of_errors : float
-        Estimated variance of errors in human scores.
+    variance_of_errors : Optional[float]
+        Estimated variance of errors in human scores. If the variance of errors
+        cannot be estimated from the data, returns ``None``.
     """
     # we first compute the total number of scores
     # available for each response
@@ -63,10 +66,9 @@ def variance_of_errors(human_scores):
     # if we don't have valid human scores
     if multiple_mask.sum() == 0:
         warnings.warn(
-            "True score evaluations cannot be "
-            "computed because none of the responses in the "
-            "evaluation set has valid "
-            "system scores and 2 human scores."
+            "True score evaluations cannot be computed because none of the "
+            "responses in the evaluation set has valid system scores and 2 "
+            "human scores."
         )
         return None
 
@@ -88,31 +90,32 @@ def variance_of_errors(human_scores):
         return variance_of_errors
 
 
-def true_score_variance(human_scores, variance_errors_human=None):
+def true_score_variance(
+    human_scores: np.ndarray, variance_errors_human: Optional[float] = None
+) -> Optional[float]:
     """
     Compute variance of true scores for multiple raters.
 
     Parameters
     ----------
-    human_scores : array-like of shape (n_samples, n_ratings)
-        Human ratings for each response.
-
-    variance_errors_human : float, optional
-        Estimated variance of errors in human scores.
-        If ``None``, the variance will be estimated
-        from the data. In this case at least some responses
-        must have more than one human score.
+    human_scores : numpy.ndarray
+        Human ratings for each response of shape (n_samples, n_ratings).
+
+    variance_errors_human : Optional[float]
+        Estimated variance of errors in human scores. If ``None``, the variance
+        will be estimated from the data. In this case at least some responses
+        *must* have more than one human score.
         Defaults to ``None``.
 
 
     Returns
     -------
-    variance_true_scores : float
-        Variance of true scores.
+    variance_true_scores : Optional[float]
+        Variance of true scores. If the variance of errors in human scores
+        is not available and cannot be estimated from the data, returns ``None``.
     """
     # if we don't have variance of errors, compute it
     # from the data
-
     if variance_errors_human is None:
         variance_errors_human = variance_of_errors(human_scores)
 
@@ -159,31 +162,32 @@ def true_score_variance(human_scores, variance_errors_human=None):
         return variance_true_scores
 
 
-def mse_true(system, human_scores, variance_errors_human=None):
+def mse_true(
+    system: np.ndarray, human_scores: np.ndarray, variance_errors_human: Optional[float] = None
+) -> Optional[float]:
     """
     Compute mean squared error (MSE) when predicting true score from system score.
 
     Parameters
     ----------
-    system : array-like of shape (n_samples,)
-        System scores for each response.
-    human_scores : array-like of shape (n_samples, n_ratings)
-        Human ratings for each response.
-    variance_errors_human : float, optional
-        Estimated variance of errors in human scores.
-        If ``None``, the variance will be estimated from
-        the data. In this case at least some responses must
-        have more than one human score.
+    system : numpy.ndarray
+        System scores for each response of shape (n_samples,).
+    human_scores : numpy.ndarray
+        Human ratings for each response of shape (n_samples, n_ratings).
+    variance_errors_human : Optional[float]
+        Estimated variance of errors in human scores. If ``None``, the variance
+        will be estimated from the data. In this case at least some responses
+        *must* have more than one human score.
         Defaults to ``None``.
 
     Returns
     -------
-    variance_true_scores : float
-        Variance of true scores.
+    variance_true_scores : Optional[float]
+        Variance of true scores. If the variance of errors in human scores
+        is not available and cannot be estimated from the data, returns ``None``.
     """
     # if we don't have variance of errors, compute it
     # from the data
-
     if variance_errors_human is None:
         variance_errors_human = variance_of_errors(human_scores)
 
@@ -205,7 +209,9 @@ def mse_true(system, human_scores, variance_errors_human=None):
     return mse
 
 
-def prmse_true(system, human_scores, variance_errors_human=None):
+def prmse_true(
+    system: np.ndarray, human_scores: np.ndarray, variance_errors_human: Optional[float] = None
+) -> Optional[float]:
     """
     Compute PRMSE when predicting true score from system scores.
 
@@ -217,21 +223,27 @@ def prmse_true(system, human_scores, variance_errors_human=None):
 
     Parameters
     ----------
-    system : array-like of shape (n_samples,)
-        System scores for each response.
-    human_scores : array-like of shape (n_samples, n_ratings)
-        Human ratings for each response.
-    variance_errors_human : float, optional
-        Estimated variance of errors in human scores.
-        If ``None``, the variance will be estimated from
-        the data. In this case at least some responses must
-        have more than one human score.
+    system : numpy.ndarray
+        System scores for each response of shape (n_samples,).
+    human_scores : numpy.ndarray
+        Human ratings for each response of shape (n_samples, n_ratings).
+    variance_errors_human : Optional[float]
+        Estimated variance of errors in human scores. If ``None``, the variance
+        will be estimated from the data. In this case at least some responses
+        *must* have more than one human score.
         Defaults to ``None``.
 
     Returns
     -------
-    prmse : float
-        Proportional reduction in mean squared error
+    prmse : Optional[float]
+        Proportional reduction in mean squared error. If the variance of errors
+        in human scores is not available and cannot be estimated from the data,
+        returns ``None``.
+
+    Raises
+    ------
+    ValueError
+        If variance of true scores or MSE could not be computed.
     """
     # check that human_scors is a two dimensional array
     # and reshape if necessary
@@ -253,39 +265,41 @@ def prmse_true(system, human_scores, variance_errors_human=None):
 
     else:
         variance_true = true_score_variance(human_scores, variance_errors_human)
-
         mse = mse_true(system, human_scores, variance_errors_human)
 
-        prmse = 1 - (mse / variance_true)
+        if variance_true is None or mse is None:
+            raise ValueError("Variance of true scores or MSE could not be computed. ")
 
+        prmse = 1 - (mse / variance_true)
         return prmse
 
 
 def get_true_score_evaluations(
-    df, system_score_columns, human_score_columns, variance_errors_human=None
-):
+    df: pd.DataFrame,
+    system_score_columns: Union[str, List[str]],
+    human_score_columns: Union[str, List[str]],
+    variance_errors_human: Optional[float] = None,
+) -> pd.DataFrame:
     """
     Generate true score evaluations for HTML reports.
 
     Parameters
     ----------
     df: pandas.DataFrame
-        Input data frame. Must contain columns listed in
-        ``system_score_columns`` and ``human_score_columns``.
-    system_score_columns: str or list
+        Input data frame. Must contain columns listed in ``system_score_columns``
+        and ``human_score_columns``.
+    system_score_columns: Union[str, List[str]
         System score column name or list of columns containing system scores.
-    human_score_columns: str or list
+    human_score_columns: Union[str, List[str]
         Human score column or list of columns containing human scores.
         True score evaluations require estimating variance of human errors,
-        which can only be computed when a subset of responses has
-        two or more human ratings. If  ``human_score_columns`` is
-        a single column name,  ``variance_errors_human`` must also
-        be specified.
-    variance_errors_human : float, optional
-        Estimated variance of errors in human scores.
-        If ``None``, the variance will be estimated from
-        the data in which case some responses must have more
-        than one human rating.
+        which can only be computed when a subset of responses has two or more
+        human ratings. If  ``human_score_columns`` is a single column name,
+        ``variance_errors_human`` *must* also be specified.
+    variance_errors_human : Optional[float]
+        Estimated variance of errors in human scores. If ``None``, the variance
+        will be estimated from the data in which case some responses *must* have
+        more than one human rating.
         Defaults to ``None``.
 
     Returns
@@ -294,17 +308,23 @@ def get_true_score_evaluations(
         DataFrame containing different evaluation metrics related to the evaluation
         of system scores against true scores. The column names are:
 
-        - "N": total number of responses
-        - "N raters": maximum number of ratings available for a single response
-        - "N_single": total number of responses with a single human score
-        - "N_multiple": total number of responses with more than one
+        - ``"N"``: total number of responses
+        - ``"N raters"``: maximum number of ratings available for a single response
+        - ``"N_single"``: total number of responses with a single human score
+        - ``"N_multiple"``: total number of responses with more than one
           human score
-        - "variance_of_errors": estimated variance of human errors
-        - "tru_var": estimated true score variance
-        - "mse_true": mean squared error when predicting true score from
+        - ``"variance_of_errors"``: estimated variance of human errors
+        - ``"tru_var"``: estimated true score variance
+        - ``"mse_true"``: mean squared error when predicting true score from
           machine score
-        - "prmse": proportional reduction in mean squared error when
+        - ``"prmse"``: proportional reduction in mean squared error when
           predicting true score
+
+    Raises
+    ------
+    ValueError
+        If ``human_score_columns`` is a single column name and ``variance_errors_human``
+        is not specified.
     """
     # check that if we only have one human column, we were also given
     # variance of errors
diff --git a/rsmtool/utils/wandb.py b/rsmtool/utils/wandb.py
index b224aab3..e1424203 100644
--- a/rsmtool/utils/wandb.py
+++ b/rsmtool/utils/wandb.py
@@ -2,8 +2,17 @@
 Utility classes and functions for logging to Weights & Biases.
 
 :author: Tamar Lavee (tlavee@ets.org)
+:author: Nitin Madnani (nmadnani@ets.org)
 """
+
+from typing import Optional, Union
+
+import pandas as pd
 import wandb
+from wandb.sdk.lib import RunDisabled
+from wandb.wandb_run import Run
+
+from ..configuration_parser import Configuration
 
 # excluded dataframes will not be logged as tables or metrics.
 # confusion matrices are logged separately.
@@ -14,21 +23,22 @@ EXCLUDED = ["confMatrix", "confMatrix_h1h2"]
 METRICS_LOGGED = ["consistency", "eval_short", "true_score_eval"]
 
 
-def init_wandb_run(config_obj):
+def init_wandb_run(config_obj: Configuration) -> Union[Run, RunDisabled, None]:
     """
-    Initialize a wandb run if logging to W&B is enabled in the configuration.
+    Initialize a wandb run if logging to wandb is enabled in the configuration.
 
-    The run object is created using the wandb project name and entity specified in
-    the configuration, and full configuration is logged to this run.
+    The Run object is created using the wandb project name and entity specified
+    in the configuration, and the full configuration is logged to this run.
 
     Parameters
     ----------
-    config_obj : configuration_parser.Configuration
-        A configuration object.
+    config_obj : Configuration
+        The configuration object containing the wandb project name and entity.
 
     Returns
     -------
-    wandb.Run : a wandb run object, or None if logging to wandb is disabled.
+    Union[wandb.wandb_run.Run, wandb.sdk.lib.RunDisabled, None]
+        A wandb Run object, or ``None`` if logging to wandb is disabled.
     """
     use_wandb = config_obj["use_wandb"]
     wandb_run = None
@@ -39,53 +49,61 @@ def init_wandb_run(config_obj):
     return wandb_run
 
 
-def log_configuration_to_wandb(wandb_run, configuration):
+def log_configuration_to_wandb(
+    wandb_run: Union[Run, RunDisabled, None], configuration: Configuration
+) -> None:
     """
-    Log a configuration object to W&B if logging to W&B is enabled.
+    Log a configuration object to wandb if logging to wandb is enabled.
 
     Parameters
     ----------
-    wandb_run : wandb.Run
-        The wandb run object, or None, if logging to W&B is disabled
+    wandb_run : Union[wandb.wandb_run.Run, wandb.sdk.lib.RunDisabled, None]
+        The wandb Run object, or ``None``, if logging to wandb is disabled.
     configuration : rsmtool.configuration_parser.Configuration
-        A Configuration object
+        The Configuration object to log to the run.
     """
     if wandb_run:
         wandb_run.config.update({configuration.context: configuration.to_dict()})
 
 
-def log_dataframe_to_wandb(wandb_run, df, df_name, section=None):
+def log_dataframe_to_wandb(
+    wandb_run: Union[Run, RunDisabled, None],
+    df: pd.DataFrame,
+    frame_name: str,
+    section: Optional[str] = None,
+) -> None:
     """
-    Log a dataframe as a table to W&B if logging to W&B is enabled.
+    Log a dataframe as a table to wandb if logging to wandb is enabled.
 
     Dataframes are logged as a table artifact. Values from selected
     dataframes will also be logged as metrics for easier comparison
-    between runs in the W&B project dashboard.
+    between runs in the wandb project dashboard.
 
     Parameters
     ----------
-    wandb_run : wandb.Run
-        The wandb run object, or None, if logging to W&B is disabled
+    wandb_run : Union[wandb.wandb_run.Run, wandb.sdk.lib.RunDisabled, None]
+        The wandb Run object, or ``None``, if logging to wandb is disabled.
     df : pandas.DataFrame
-        The dataframe object
-    df_name : str
-        The name of the dataframe
+        The dataframe object to log.
+    frame_name : str
+        The name of the dataframe to use in the log.
     section : str
         The section in which the dataframe will we logged. If set to ``None``,
-        it will be logged to the default "Charts" section. Defaults to ``None``.
+        it will be logged to the default "Charts" section.
+        Defaults to ``None``.
     """
-    if wandb_run and df_name not in EXCLUDED:
+    if wandb_run and frame_name not in EXCLUDED:
         table = wandb.Table(dataframe=df, allow_mixed_types=True)
-        name = f"{section}/{df_name}" if section is not None else df_name
+        name = f"{section}/{frame_name}" if section is not None else frame_name
         wandb_run.log({name: table})
-        if df_name in METRICS_LOGGED:
+        if frame_name in METRICS_LOGGED:
             indexed_df = df.set_index(df.columns[0])
             metric_dict = {}
             for column in indexed_df.columns:
                 col_dict = indexed_df[column].to_dict()
                 metric_dict.update(
                     {
-                        get_metric_name(section, df_name, column, row): value
+                        get_metric_name(section, frame_name, column, row): value
                         for row, value in col_dict.items()
                         if value
                     }
@@ -93,9 +111,11 @@ def log_dataframe_to_wandb(wandb_run, df, df_name, section=None):
             wandb_run.log(metric_dict)
 
 
-def get_metric_name(section, df_name, col_name, row_name):
+def get_metric_name(
+    section: Optional[str], frame_name: str, col_name: str, row_name: Union[int, str]
+) -> str:
     """
-    Generate the metric name for logging in W&B.
+    Generate the metric name for logging in wandb.
 
     The name contains the dataframe, column and row names,
     unless row name is empty or 0 (when dataframe has a single line)
@@ -104,21 +124,22 @@ def get_metric_name(section, df_name, col_name, row_name):
 
     Parameters
     ----------
-    section : str
-        The section in which the dataframe will we logged
-    df_name : str
-        The dataframe name
+    section : Optional[str]
+        The section name to use in the log. If set to ``None``,
+        it will not be added to the metric name.
+    frame_name : str
+        The dataframe name to use in the log.
     col_name : str
-        The column name
-    row_name : Union[int,str]
+        The column name to use in the log.
+    row_name : Union[int, str]
         The row name, or 0 for some single line dataframes.
 
     Returns
     -------
     metric_name : str
-        The metric name
+        The metric name to be logged.
     """
-    metric_name = f"{df_name}.{col_name}"
+    metric_name = f"{frame_name}.{col_name}"
     if section is not None and section != "":
         metric_name = f"{section}/{metric_name}"
     if row_name != "" and row_name != 0:
@@ -126,16 +147,22 @@ def get_metric_name(section, df_name, col_name, row_name):
     return metric_name
 
 
-def log_confusion_matrix(wandb_run, human_scores, system_scores, name, section):
+def log_confusion_matrix(
+    wandb_run: Union[Run, RunDisabled, None],
+    human_scores: pd.Series,
+    system_scores: pd.Series,
+    name: str,
+    section: str,
+) -> None:
     """
-    Log a confusion matrix to W&B if logging to W&B is enabled.
+    Log a confusion matrix to wandb if logging to wandb is enabled.
 
     The confusion matrix is added as a custom chart.
 
     Parameters
     ----------
-    wandb_run : wandb.Run
-        The wandb run object, or None, if logging to W&B is disabled
+    wandb_run : Union[wandb.wandb_run.Run, wandb.sdk.lib.RunDisabled, None]
+        The wandb Run object, or ``None``, if logging to wandb is disabled.
     human_scores : pandas.Series
         The human scores for the responses in the data
     system_scores : pandas.Series
@@ -158,21 +185,23 @@ def log_confusion_matrix(wandb_run, human_scores, system_scores, name, section):
         )
 
 
-def log_report_to_wandb(wandb_run, report_name, report_path):
+def log_report_to_wandb(
+    wandb_run: Union[Run, RunDisabled, None], report_name: str, report_path: str
+):
     """
-    Log a report to W&B if logging to W&B is enabled.
+    Log a report to wandb if logging to wandb is enabled.
 
     The report is logged both as an artifact and as an HTML file.
     The html is logged to a section called "reports".
 
     Parameters
     ----------
-    wandb_run : wandb.Run
-        The wandb run object, or None, if logging to W&B is disabled
+    wandb_run : Union[wandb.wandb_run.Run, wandb.sdk.lib.RunDisabled, None]
+        The wandb Run object, or ``None``, if logging to wandb is disabled.
     report_name: str
-        The report's name
+        The report's name to use in the log.
     report_path : str
-        The path to the report html file.
+        The path to the report html file that is to be logged.
     """
     if wandb_run:
         with open(report_path, "r") as rf:
diff --git a/rsmtool/writer.py b/rsmtool/writer.py
index 4b7e01f2..7a4ad4fb 100644
--- a/rsmtool/writer.py
+++ b/rsmtool/writer.py
@@ -13,6 +13,7 @@ from os.path import join
 from typing import Dict, List, Optional, Union
 
 import pandas as pd
+from wandb.sdk.lib import RunDisabled
 from wandb.wandb_run import Run
 
 from rsmtool.container import DataContainer
@@ -27,7 +28,7 @@ class DataWriter:
         self,
         experiment_id: Optional[str] = None,
         context: Optional[str] = None,
-        wandb_run: Optional[Run] = None,
+        wandb_run: Union[Run, RunDisabled, None] = None,
     ):
         """
         Initialize the DataWriter object.
@@ -40,7 +41,7 @@ class DataWriter:
         context : Optional[str]
             The context in which this writer is used.
             Defaults to ``None``.
-        wandb_run : Optional[wandb.wandb_run.Run]
+        wandb_run : Union[wandb.wandb_run.Run, wandb.sdk.lib.RunDisabled, None]
             The wandb run object if wandb is enabled, None otherwise.
             If enabled, all the output data frames will be logged to
             this run as tables.


Test patch:
diff --git a/rsmtool/test_utils.py b/rsmtool/test_utils.py
index f2609fff..a1fb2419 100644
--- a/rsmtool/test_utils.py
+++ b/rsmtool/test_utils.py
@@ -1223,7 +1223,7 @@ def check_generated_output(
         The experiment ID.
     model_source : str
         One of ``"rsmtool"`` or ``"skll"``.
-    file_format : str, optional
+    file_format : str
         The format of the output files.
         Defaults to ``"csv"``.
     """
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 2218022a..69b32ee9 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -93,8 +93,8 @@ class TestUtils(unittest.TestCase):
         self.assertEqual(convert_to_float(5.0), 5.0)
 
     def test_parse_range(self):
-        self.assertEqual(parse_range("5-10"), [5, 10])
-        self.assertEqual(parse_range("0-100"), [0, 100])
+        self.assertEqual(parse_range("5-10"), (5, 10))
+        self.assertEqual(parse_range("0-100"), (0, 100))
 
     def test_parse_range_invalid_1(self):
         with self.assertRaises(ValueError):