What is class_ for a dictionary of lists of strings?

I’m working on some code that uses Python’s param package to initialize variables:

import param

class UnivariateConfig(param.Parameterized):
    input_directory = param.String(default='')
    output_directory = param.String(default='outputs')
    univariate_config = param.String(default='')
    time_types = param.List(default=[], class_=str)
    questionnaire_types = param.List(default=[], class_=str)
    label_types = param.List(default=[], class_=str)

I’m struggling with the param documentation, I cannot find what the values of class_ should be for a dictionary I would normally type as

model_types: Dict[str, List[str]] = {}

How would I rewrite that line to use param.Dict, i.e. what should I write in place of WhichClass in this:

model_types = param.Dict(default={}, class_=WhichClass)
1 Like

Hi. I am not sure if I can help you but in any case I don’t think it is entirely clear what you want. Why don’t you leave out the class_ kwarg and let it set to Dict automatically? What part of your code that uses model_types later on actually fails?

Hi @dumbledad

That is a good question. I’ve often thought about how to define a param.Parameter of a complex type. Here are my thoughts and observations.

param.Dict constructor has no class_ argument

The param.Dict class inherits from the param.ClassSelector and as such has already set the class_ argument to dict.

The above is all the code defining param.Dict.

The inheritance chain is Dict <- ClassSelector <- SelectorBase <- Parameter.

The validation is done in the _validate function.

Parameter has a _validate function that ClassSelector implements. This function you could override.

Then the question is which Class to override: Dict, ClassSelector, SelectorBase or Parameter.

For not knowing better I would choose the simplest solution Parameter and look at how String is implemented.

How to test for Dict[str, List[str]]?

You can get inspiration from https://stackoverflow.com/questions/58985396/python-check-if-dictionary-value-is-made-of-list-of-strings

Example Implementation

import param

class DictWithStringKeyAndListOfStringsValues(param.Parameter):
    def __init__(self, default={}, allow_None=False, **kwargs):
        super().__init__(default=default, allow_None=allow_None, **kwargs)

        # I can see that the `Parameter` __init__ does not `_validate` the default
        # Is that a bug?
        self._validate(self.default)

    def _validate(self, val):
        if self.allow_None and val is None:
            return

        if not isinstance(val, dict):
            raise ValueError(f"Error. The value {val} is not of type Dict[str, List]!")

        for key in val.keys():
            if not isinstance(key, str):
                raise ValueError(f"Error. The key {key} is not of type str!")
        for value in val.values():
            if not isinstance(value, list):
                raise ValueError(f"Error. The dictionary value {value} is not of type list!")
            for list_val in value:
                if not isinstance(list_val, str):
                    raise ValueError(f"Error. The list value {list_val} is not of type str!")

Test of Example Implementation

If I add the below tests and run Pytest I get

import pytest

class UnivariateConfig(param.Parameterized):
    model_types = DictWithStringKeyAndListOfStringsValues(default={})

@pytest.mark.parametrize(["value"], [
    ({}, ),
    ({"a": []}, ),
    ({"a": ["b"]}, ),
])
def test_does_not_raise_error_when_correct_type(value):
    config = UnivariateConfig(model_types=value)
    assert config.model_types == value

@pytest.mark.parametrize(["value"], [
    (None, ),
    ("str", ),
    ({"a": [None]}, ),
])
def test_raises_value_error_if_not_correct_type(value):
    with pytest.raises(ValueError):
        UnivariateConfig(model_types=value)
Test session starts (platform: win32, Python 3.7.6, pytest 5.4.2, pytest-sugar 0.9.3)
cachedir: .pytest_cache
rootdir: C:\repos\private\awesome-panel, inifile: pytest.ini
plugins: cov-2.8.1, mock-3.1.0, sugar-0.9.3
collecting ... 
 scripts\issue_param_class.py::test_does_not_raise_error_when_correct_type[value0] ✓                                                                                                       17% █▋

 scripts\issue_param_class.py::test_does_not_raise_error_when_correct_type[value1] ✓                                                                                                       33% ███▍

 scripts\issue_param_class.py::test_does_not_raise_error_when_correct_type[value2] ✓                                                                                                       50% █████

 scripts\issue_param_class.py::test_raises_value_error_if_not_correct_type[None] ✓                                                                                                         67% ██████▋

 scripts\issue_param_class.py::test_raises_value_error_if_not_correct_type[str] ✓                                                                                                          83% ████████▍

 scripts\issue_param_class.py::test_raises_value_error_if_not_correct_type[value2] ✓                                                                                                      100% ██████████

Results (0.23s):
       6 passed

Final Reflections

The above validation might slow down your code, so it depends on your use case if you would like that extra validation. Maybe it’s best just to go with param.Dict as @Leonidas says?

Note that we have often though about allowing composing param types and it’s definitely on the roadmap, e.g. you should be able to declare a param.List(type=param.Number()) or a param.Tuple(type=(param.Number(), param.String()). There’s some refactoring we have to do to make that happen but overall this doesn’t actually seem that difficult.

1 Like