from typing import List
from typing import Any
import warnings


def _convert(values: List[str], dtypes: List[type], strict: bool = False) -> List[Any]:
    """
    Cast numerical values in a list of strings to float or int
    as specified by the dtypes parameter.

    :param values: A list of strings representing a row in a SQL table
        E.g. ['28207', 'April', '4742783', '0.9793'].
    :type values: List[str]
    :param dtypes: A list of Python data types. E.g. [int, str, int, float]
    :type dtypes: List[type]
    :param strict: When set to False, if any of the items in the list
        cannot be converted, it is returned unchanged, i.e. as a str.
    :type strict: bool, optional
    :raises ValueError: If `values` is not the same length as `dtypes`,
        or if `strict` is set to True and some of the values in the
        list couldn't be converted.
    :return: A list where the numerical values have been cast as int
        or string as defined by `dtypes`. E.g. the example list from
        above is returned as [28207, 'April', 4742783, 0.9793]
    :rtype: List[Any]
    """

    len_values = len(values)
    len_dtypes = len(dtypes)

    warn = False

    if len_values != len_dtypes:
        if not strict:
            return values

        raise ValueError("values and dtypes are not the same length")

    converted = []
    for i in range(len_dtypes):
        dtype = dtypes[i]
        val = values[i]

        try:
            conv = dtype(val)
            converted.append(conv)

        except ValueError as e:
            if values[i] == "":
                # why not convert to None?
                converted.append(val)
            elif not strict:
                warn = True
                converted.append(val)
            else:
                print(f"ValueError: {e}")
                raise

    if warn:
        warnings.warn(
            "some values could not be converted to Python dtypes", UserWarning
        )

    return converted