existing - chokes if value is a list self._check_value(action, value) proposed change (does not convert strings, but check_value does ensure that values are of the correct type) if not isinstance(value, list): self._check_value(action, value) else: for v in value: self._check_value(action, v) minimal change - avoids the choke without changing anything else if not isinstance(value, list): self._check_value(action, value) closer to other defaults - convert string but don't test it use other objects as is if isinstance(value, str): value = self._get_value(action, value) closer to ?positional - convert and test string if isinstance(value, str): value = self._get_value(action, value) self._check_value(action, value) no action - simply use values (passes tests) # self._check_value(action, value) convert and check string; only check list values if not isinstance(value, list): value = self._get_value(action, value) self._check_value(action, value) else: for v in value: self._check_value(action, v) maximal change (convert and check each value) if not isinstance(value, list): value = self._get_value(action, value) self._check_value(action, value) else: value = [self.get_value(action, v) for v in value] for v in value: self._check_value(action, v) more robust if isinstance(value, str): value = self._get_value(action, value) self._check_value(action, value) elif isinstance(value, (tuple, list)): # more generally, iterable def foo(v): if isinstance(v, str): return self._get_value(actin, v) else: return v value = [foo(v) for v in value] for v in value: self._check_value(action, value) else: # anything else, just use (or object) pass A variation on _check_value() could take a list, test them individually and include all failures in the error message A key point in _get_values 'arg_strings' is always a list of strings, possibly empty 'default' may be string, int, float, list, other object _get_value, and type expect a string; they may work with other things (e.g. int(1)), but that is not guaranteed normal default behavior is to pass string default through type and simply assign anything else. The latest delayed default evaluation: if action is not seen default is str (further dest tests) _get_value(action, default) These ?* positionals are always 'seen', hence not processed in the 'delayed' block