environs icon indicating copy to clipboard operation
environs copied to clipboard

Default values and subcast

Open tobiasmboelz opened this issue 3 years ago • 2 comments

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).

tobiasmboelz avatar Sep 01 '22 17:09 tobiasmboelz

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?

BroFromSpace avatar Sep 12 '24 16:09 BroFromSpace

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.

tobiasmboelz avatar Sep 14 '24 15:09 tobiasmboelz

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

sloria avatar Jan 07 '25 22:01 sloria