Repository: marshmallow-code/marshmallow
Pull request #1742 (2021-02-09 08:10:13)

Replace missing/default with {load,dump}_default
This is part of #778 , which has been open but inactive for a while.
I've made aggressive use of warnings here and chose the `load_default` and `dump_default` names (of the options proposed) because they are parallel with `load_key`, `dump_only`, etc in putting the de/serialization qualifier first.

I expect that if this is released, a lot of users of libraries like apispec would start seeing warnings.
I'm not sure what the best way to mitigate that is, but if removing or changing warnings on some of these behaviors would improve things, just let me know.

---

Add these new parameters to the Field initializer and to the attributes of all fields. Remove the existing `missing/default` attributes in lieu of the new values.

If a 'default' or 'missing' value is passed, respect it but emit a warning.

New `default` and `missing` field properties wrap access to load_default and dump_default, but also emit warnings on access.

Update all usage in tests to the newer form.

lafrech: > I expect that if this is released, a lot of users of libraries like apispec would start seeing warnings.

We can ship a major using new param names. Or a minor with a condition to pick the appropriate parameter according to marshmallow version (extra work, but a major just to bump marshmallow version is not appealing).
lafrech: I'm still willing to do this name change.

Question is should we postpone it to v4 or deprecate/warn (this PR) now and remove in v4.

I don't mind either way. Maybe we're overthinking it with the deprecation/warning cycle. OTOH, the job is done already.

We're about to ship webargs 8 so now would be a good time to drop the old names.

I had a quick look at apispec / webargs / flask-smorest and I think it should be easy to support without a breaking change by updating the tests / docstrings and add a few conditions to support the old names (not so many places).
sirosen: If a deprecation warning is a big enough deal to potentially hold back this change, I am open to removing the warnings and leaving the deprecation in the docs only.

I'd really prefer to use `load_default` and `dump_default`. The `default`/`missing` names today are just too easy to mix up.
lafrech: No, those warnings are good. Better than doc only. The alternative was postpone to v4. But I prefer to merge this now.
sirosen: I just rebased this to cleanup the conflict.

After more thought, I think the easiest thing is for downstream projects to do
```
if MARSHMALLOW_VERSION < (3,12):
    ...
else:
    ...
```
as necessary if they want to avoid the warnings and be compatible with lower marshmallow 3.x versions.

Patch:
diff --git a/src/marshmallow/fields.py b/src/marshmallow/fields.py
index 437d339e..88c1bc71 100644
--- a/src/marshmallow/fields.py
+++ b/src/marshmallow/fields.py
@@ -76,10 +76,10 @@ class Field(FieldABC):
     data does not need to be formatted before being serialized or deserialized.
     On error, the name of the field will be returned.
 
-    :param default: If set, this value will be used during serialization if the input value
-        is missing. If not set, the field will be excluded from the serialized output if the
-        input value is missing. May be a value or a callable.
-    :param missing: Default deserialization value for the field if the field is not
+    :param dump_default: If set, this value will be used during serialization if the
+        input value is missing. If not set, the field will be excluded from the
+        serialized output if the input value is missing. May be a value or a callable.
+    :param load_default: Default deserialization value for the field if the field is not
         found in the input data. May be a value or a callable.
     :param data_key: The name of the dict key in the external representation, i.e.
         the input of `load` and the output of `dump`.
@@ -148,8 +148,10 @@ class Field(FieldABC):
     def __init__(
         self,
         *,
-        default: typing.Any = missing_,
+        load_default: typing.Any = missing_,
         missing: typing.Any = missing_,
+        dump_default: typing.Any = missing_,
+        default: typing.Any = missing_,
         data_key: typing.Optional[str] = None,
         attribute: typing.Optional[str] = None,
         validate: typing.Optional[
@@ -166,7 +168,26 @@ class Field(FieldABC):
         metadata: typing.Optional[typing.Mapping[str, typing.Any]] = None,
         **additional_metadata
     ) -> None:
-        self.default = default
+        # handle deprecated `default` and `missing` parameters
+        if default is not missing_:
+            warnings.warn(
+                "The 'default' argument to fields is deprecated. "
+                "Use 'dump_default' instead.",
+                RemovedInMarshmallow4Warning,
+            )
+            if dump_default is missing_:
+                dump_default = default
+        if missing is not missing_:
+            warnings.warn(
+                "The 'missing' argument to fields is deprecated. "
+                "Use 'load_default' instead.",
+                RemovedInMarshmallow4Warning,
+            )
+            if load_default is missing_:
+                load_default = missing
+        self.dump_default = dump_default
+        self.load_default = load_default
+
         self.attribute = attribute
         self.data_key = data_key
         self.validate = validate
@@ -182,15 +203,14 @@ class Field(FieldABC):
                 "or a collection of callables."
             )
 
-        # If allow_none is None and missing is None
+        # If allow_none is None and load_default is None
         # None should be considered valid by default
-        self.allow_none = missing is None if allow_none is None else allow_none
+        self.allow_none = load_default is None if allow_none is None else allow_none
         self.load_only = load_only
         self.dump_only = dump_only
-        if required is True and missing is not missing_:
-            raise ValueError("'missing' must not be set for required fields.")
+        if required is True and load_default is not missing_:
+            raise ValueError("'load_default' must not be set for required fields.")
         self.required = required
-        self.missing = missing
 
         metadata = metadata or {}
         self.metadata = {**metadata, **additional_metadata}
@@ -213,11 +233,11 @@ class Field(FieldABC):
 
     def __repr__(self) -> str:
         return (
-            "<fields.{ClassName}(default={self.default!r}, "
+            "<fields.{ClassName}(dump_default={self.dump_default!r}, "
             "attribute={self.attribute!r}, "
             "validate={self.validate}, required={self.required}, "
             "load_only={self.load_only}, dump_only={self.dump_only}, "
-            "missing={self.missing}, allow_none={self.allow_none}, "
+            "load_default={self.load_default}, allow_none={self.allow_none}, "
             "error_messages={self.error_messages})>".format(
                 ClassName=self.__class__.__name__, self=self
             )
@@ -309,7 +329,7 @@ class Field(FieldABC):
         if self._CHECK_ATTRIBUTE:
             value = self.get_value(obj, attr, accessor=accessor)
             if value is missing_:
-                default = self.default
+                default = self.dump_default
                 value = default() if callable(default) else default
             if value is missing_:
                 return value
@@ -337,7 +357,7 @@ class Field(FieldABC):
         # deserialized value
         self._validate_missing(value)
         if value is missing_:
-            _miss = self.missing
+            _miss = self.load_default
             return _miss() if callable(_miss) else _miss
         if self.allow_none and value is None:
             return None
@@ -411,6 +431,44 @@ class Field(FieldABC):
         """The context dictionary for the parent :class:`Schema`."""
         return self.parent.context
 
+    # the default and missing properties are provided for compatibility and
+    # emit warnings when they are accessed and set
+    @property
+    def default(self):
+        warnings.warn(
+            "The 'default' attribute of fields is deprecated. "
+            "Use 'dump_default' instead.",
+            RemovedInMarshmallow4Warning,
+        )
+        return self.dump_default
+
+    @default.setter
+    def default(self, value):
+        warnings.warn(
+            "The 'default' attribute of fields is deprecated. "
+            "Use 'dump_default' instead.",
+            RemovedInMarshmallow4Warning,
+        )
+        self.dump_default = value
+
+    @property
+    def missing(self):
+        warnings.warn(
+            "The 'missing' attribute of fields is deprecated. "
+            "Use 'load_default' instead.",
+            RemovedInMarshmallow4Warning,
+        )
+        return self.load_default
+
+    @missing.setter
+    def missing(self, value):
+        warnings.warn(
+            "The 'missing' attribute of fields is deprecated. "
+            "Use 'load_default' instead.",
+            RemovedInMarshmallow4Warning,
+        )
+        self.load_default = value
+
 
 class Raw(Field):
     """Field that applies no formatting."""
@@ -467,6 +525,7 @@ class Nested(Field):
         self,
         nested: typing.Union[SchemaABC, type, str, typing.Callable[[], SchemaABC]],
         *,
+        dump_default: typing.Any = missing_,
         default: typing.Any = missing_,
         only: typing.Optional[types.StrSequenceOrSet] = None,
         exclude: types.StrSequenceOrSet = (),
@@ -493,7 +552,7 @@ class Nested(Field):
         self.many = many
         self.unknown = unknown
         self._schema = None  # Cached Schema instance
-        super().__init__(default=default, **kwargs)
+        super().__init__(default=default, dump_default=dump_default, **kwargs)
 
     @property
     def schema(self):
@@ -1892,8 +1951,8 @@ class Constant(Field):
     def __init__(self, constant: typing.Any, **kwargs):
         super().__init__(**kwargs)
         self.constant = constant
-        self.missing = constant
-        self.default = constant
+        self.load_default = constant
+        self.dump_default = constant
 
     def _serialize(self, value, *args, **kwargs):
         return self.constant


Test patch:
diff --git a/tests/base.py b/tests/base.py
index 707708cf..297f2a6b 100644
--- a/tests/base.py
+++ b/tests/base.py
@@ -163,7 +163,7 @@ class UserSchema(Schema):
     created_iso = fields.DateTime(format="iso", attribute="created", dump_only=True)
     updated = fields.DateTime()
     species = fields.String(attribute="SPECIES")
-    id = fields.String(default="no-id")
+    id = fields.String(dump_default="no-id")
     uppername = Uppercased(attribute="name", dump_only=True)
     homepage = fields.Url()
     email = fields.Email()
diff --git a/tests/test_deserialization.py b/tests/test_deserialization.py
index 3abe99a9..c8a51464 100644
--- a/tests/test_deserialization.py
+++ b/tests/test_deserialization.py
@@ -27,7 +27,7 @@ class TestDeserializingNone:
             field.deserialize(None)
 
     def test_allow_none_is_true_if_missing_is_true(self):
-        field = fields.Field(missing=None)
+        field = fields.Field(load_default=None)
         assert field.allow_none is True
         assert field.deserialize(None) is None
 
@@ -1513,7 +1513,7 @@ class TestSchemaDeserialization:
 
         class AliasingUserSerializer(Schema):
             name = fields.String()
-            birthdate = fields.DateTime(missing=bdate)
+            birthdate = fields.DateTime(load_default=bdate)
 
         data = {"name": "Mick"}
         result = AliasingUserSerializer().load(data)
@@ -1525,7 +1525,7 @@ class TestSchemaDeserialization:
 
         class AliasingUserSerializer(Schema):
             name = fields.String()
-            birthdate = fields.DateTime(missing=lambda: bdate)
+            birthdate = fields.DateTime(load_default=lambda: bdate)
 
         data = {"name": "Mick"}
         result = AliasingUserSerializer().load(data)
@@ -1535,7 +1535,7 @@ class TestSchemaDeserialization:
     def test_deserialize_with_missing_param_none(self):
         class AliasingUserSerializer(Schema):
             name = fields.String()
-            years = fields.Integer(missing=None, allow_none=True)
+            years = fields.Integer(load_default=None, allow_none=True)
 
         data = {"name": "Mick"}
         result = AliasingUserSerializer().load(data)
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 123367a2..e7f552bd 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -30,12 +30,12 @@ def test_field_aliases(alias, field):
 class TestField:
     def test_repr(self):
         default = "œ∑´"
-        field = fields.Field(default=default, attribute=None)
+        field = fields.Field(dump_default=default, attribute=None)
         assert repr(field) == (
-            "<fields.Field(default={0!r}, attribute=None, "
+            "<fields.Field(dump_default={0!r}, attribute=None, "
             "validate=None, required=False, "
             "load_only=False, dump_only=False, "
-            "missing={missing}, allow_none=False, "
+            "load_default={missing}, allow_none=False, "
             "error_messages={error_messages})>".format(
                 default, missing=missing, error_messages=field.error_messages
             )
@@ -49,9 +49,9 @@ class TestField:
 
     def test_error_raised_if_missing_is_set_on_required_field(self):
         with pytest.raises(
-            ValueError, match="'missing' must not be set for required fields"
+            ValueError, match="'load_default' must not be set for required fields"
         ):
-            fields.Field(required=True, missing=42)
+            fields.Field(required=True, load_default=42)
 
     def test_custom_field_receives_attr_and_obj(self):
         class MyField(fields.Field):
@@ -204,7 +204,7 @@ class TestMetadata:
         assert field.metadata["description"] == "Just a normal field."
         field = FieldClass(
             required=True,
-            default=None,
+            dump_default=None,
             validate=lambda v: True,
             metadata={"description": "foo", "widget": "select"},
         )
@@ -220,7 +220,7 @@ class TestMetadata:
         with pytest.warns(DeprecationWarning):
             field = FieldClass(
                 required=True,
-                default=None,
+                dump_default=None,
                 validate=lambda v: True,
                 description="foo",
                 metadata={"widget": "select"},
@@ -228,6 +228,62 @@ class TestMetadata:
         assert field.metadata == {"description": "foo", "widget": "select"}
 
 
+class TestDeprecatedDefaultAndMissing:
+    @pytest.mark.parametrize("FieldClass", ALL_FIELDS)
+    def test_load_default_in_deprecated_style_warns(self, FieldClass):
+        # in constructor
+        with pytest.warns(
+            DeprecationWarning,
+            match="The 'missing' argument to fields is deprecated. "
+            "Use 'load_default' instead.",
+        ):
+            FieldClass(missing=None)
+
+        # via attribute
+        myfield = FieldClass(load_default=1)
+        with pytest.warns(
+            DeprecationWarning,
+            match="The 'missing' attribute of fields is deprecated. "
+            "Use 'load_default' instead.",
+        ):
+            assert myfield.missing == 1
+        with pytest.warns(
+            DeprecationWarning,
+            match="The 'missing' attribute of fields is deprecated. "
+            "Use 'load_default' instead.",
+        ):
+            myfield.missing = 0
+        # but setting it worked
+        assert myfield.load_default == 0
+
+    @pytest.mark.parametrize("FieldClass", ALL_FIELDS)
+    def test_dump_default_in_deprecated_style_warns(self, FieldClass):
+        # in constructor
+        with pytest.warns(
+            DeprecationWarning,
+            match="The 'default' argument to fields is deprecated. "
+            "Use 'dump_default' instead.",
+        ):
+            FieldClass(default=None)
+
+        # via attribute
+        myfield = FieldClass(dump_default=1)
+        with pytest.warns(
+            DeprecationWarning,
+            match="The 'default' attribute of fields is deprecated. "
+            "Use 'dump_default' instead.",
+        ):
+            assert myfield.default == 1
+        with pytest.warns(
+            DeprecationWarning,
+            match="The 'default' attribute of fields is deprecated. "
+            "Use 'dump_default' instead.",
+        ):
+            myfield.default = 0
+        # but setting it worked
+        assert myfield.dump_default == 0
+
+
 class TestErrorMessages:
     class MyField(fields.Field):
         default_error_messages = {"custom": "Custom error message."}
diff --git a/tests/test_schema.py b/tests/test_schema.py
index 4428b4f5..edba9e6d 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -326,7 +326,7 @@ def test_loads_deserializes_from_json():
 
 def test_serializing_none():
     class MySchema(Schema):
-        id = fields.Str(default="no-id")
+        id = fields.Str(dump_default="no-id")
         num = fields.Int()
         name = fields.Str()
 
@@ -1995,7 +1995,7 @@ class TestNestedSchema:
 
     def test_nested_none(self):
         class BlogDefaultSchema(Schema):
-            user = fields.Nested(UserSchema, default=0)
+            user = fields.Nested(UserSchema, dump_default=0)
 
         b = Blog("Just the default blog", user=None)
         data = BlogDefaultSchema().dump(b)
@@ -2755,8 +2755,8 @@ class TestDefaults:
         list_no_default = fields.List(fields.Str, allow_none=True)
         nested_no_default = fields.Nested(UserSchema, many=True, allow_none=True)
 
-        int_with_default = fields.Int(allow_none=True, default=42)
-        str_with_default = fields.Str(allow_none=True, default="foo")
+        int_with_default = fields.Int(allow_none=True, dump_default=42)
+        str_with_default = fields.Str(allow_none=True, dump_default="foo")
 
     @pytest.fixture()
     def schema(self):
diff --git a/tests/test_serialization.py b/tests/test_serialization.py
index f0e2b614..ca6fd0b9 100644
--- a/tests/test_serialization.py
+++ b/tests/test_serialization.py
@@ -125,14 +125,14 @@ class TestFieldSerialization:
 
     def test_integer_field_default(self, user):
         user.age = None
-        field = fields.Integer(default=0)
+        field = fields.Integer(dump_default=0)
         assert field.serialize("age", user) is None
         # missing
         assert field.serialize("age", {}) == 0
 
     def test_integer_field_default_set_to_none(self, user):
         user.age = None
-        field = fields.Integer(default=None)
+        field = fields.Integer(dump_default=None)
         assert field.serialize("age", user) is None
 
     def test_uuid_field(self, user):
@@ -594,7 +594,7 @@ class TestFieldSerialization:
         assert field.serialize("name", user) is None
 
     def test_string_field_default_to_empty_string(self, user):
-        field = fields.String(default="")
+        field = fields.String(dump_default="")
         assert field.serialize("notfound", {}) == ""
 
     def test_time_field(self, user):
@@ -857,7 +857,7 @@ class TestSchemaSerialization:
     def test_serialize_with_missing_param_value(self):
         class AliasingUserSerializer(Schema):
             name = fields.String()
-            birthdate = fields.DateTime(default=dt.datetime(2017, 9, 29))
+            birthdate = fields.DateTime(dump_default=dt.datetime(2017, 9, 29))
 
         data = {"name": "Mick"}
         result = AliasingUserSerializer().dump(data)
@@ -867,7 +867,7 @@ class TestSchemaSerialization:
     def test_serialize_with_missing_param_callable(self):
         class AliasingUserSerializer(Schema):
             name = fields.String()
-            birthdate = fields.DateTime(default=lambda: dt.datetime(2017, 9, 29))
+            birthdate = fields.DateTime(dump_default=lambda: dt.datetime(2017, 9, 29))
 
         data = {"name": "Mick"}
         result = AliasingUserSerializer().dump(data)