Default values and subcast
As far as I can tell the default value can either be of the cast type or a string. So env.bool('FOO', True), env.bool('FOO', 'true'), env.bool('FOO', '1') and env.bool('FOO', 'YES') all are basically the same.
>>> os.environ.get('FOO')
>>> env.bool('FOO', True)
True
>>> env.bool('FOO', 'true')
True
>>> env.bool('FOO', '1')
True
>>> env.bool('FOO', 'YES')
True
The same goes for lists with a subcast to a basic type. env.list('FOO', '1, 2, 42', subcast=float) and env.list('FOO', [1.0, 2.0, 42.0], subcast=float) both do work.
>>> env.list('FOO', '1, 2, 42', subcast=float)
[1.0, 2.0, 42.0]
>>> env.list('FOO', [1.0, 2.0, 42.0], subcast=float)
[1.0, 2.0, 42.0]
But, using the target type as default only works as long as the subcast type/callable can be called with the cast type itself. It breaks when the callable expects a string and returns something else. For example:
>>> env.list('FOO', 'a:b, b:c', subcast=lambda s: tuple(s.split(':')))
[('a', 'b'), (' b', 'c')]
>>> env.list('FOO', ['a:b', 'b:c'], subcast=lambda s: tuple(s.split(':')))
[('a', 'b'), ('b', 'c')]
>>> env.list('FOO', [('a', 'b'), ('b', 'c')], subcast=lambda s: tuple(s.split(':')))
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "[…]\lib\site-packages\environs\__init__.py", line 123, in method
value = field.deserialize(value)
File "[…]\lib\site-packages\marshmallow\fields.py", line 368, in deserialize
output = self._deserialize(value, attr, data, **kwargs)
File "[…]\lib\site-packages\marshmallow\fields.py", line 784, in _deserialize
result.append(self.inner.deserialize(each, **kwargs))
File "[…]\lib\site-packages\marshmallow\fields.py", line 368, in deserialize
output = self._deserialize(value, attr, data, **kwargs)
File "[…]\lib\site-packages\environs\__init__.py", line 188, in _deserialize
return func(value)
File "<console>", line 1, in <lambda>
AttributeError: 'tuple' object has no attribute 'split'
If this is expected behaviour, I’d be happy to provide a pull request with a supplement to the documentation (well, README.md).
Am I correct in understanding that the subcast is applied to the default value? If so, does this make sense, since a default value is supposed to be predictable and shouldn't require any transformations? Wouldn't it be better to simply check if the default value is of the correct type or None?
The subcast is applied to elements of the list after splitting.
Env.list has special handling for the case that a list (or any other iterable except string) is passed in[^1], but subcast has not. I'd suggest that, for consistency, the subcast should only be applied if the value is a string.
As for the suggestion to check if the default value is of the correct type: That would only work as long as the subcast argument is a type. For (other) callables, that would require either another argument or a mandatory return annotation. IMO, both would make usage of the package unnecessarily complex.
[^1]: So does Env.dict.
Am I correct in understanding that the subcast is applied to the default value? If so, does this make sense, since a default value is supposed to be predictable and shouldn't require any transformations?
your understanding is correct. in retrospect, allowing serialized values as default values was a poor design choice. #379 changes the logic so that default values are always expected to be in their deserialized form