Repository: marshmallow-code/marshmallow
Pull request #2722 (2025-01-04 19:59:38)

Remove FieldABC and the base module
refactors the code so that `FieldABC` and the entire `base` module are no longer needed.


note: this is technically backwards incompatible because `resolve_field_instance` and `FieldInstanceResolutionError` are privatized but I wouldn't expect anyone to actually be using those

sloria: gonna go ahead and merge this one because i'm reasonably confident in it and it will unblock some further refactoring i want to do

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 7df60c2b..e25d93be 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -53,6 +53,7 @@ Deprecations/Removals:
 - *Backwards-incompatible*: Remove implicit field creation, i.e. using the ``fields`` or ``additional`` class Meta options with undeclared fields (:issue:`1356`).
 - The `ordered` class Meta option is removed  (:issue:`2146`). Field order is already preserved by default.
   Set `Schema.dict_class` to `OrderedDict` to maintain the previous behavior.
+- The `marshmallow.base` module is removed (:pr:`2722`).
 
 Previously-deprecated API have been removed, including:
 
@@ -75,7 +76,7 @@ Features:
 
 Bug fixes:
 
-- Typing: Fix type hint for ``nested`` parameter of `Nested <marshmallow.fields.Nested>`.
+- Typing: Fix type hint for ``nested`` parameter of `Nested <marshmallow.fields.Nested>` (:pr:`2721`).
 
 
 3.23.3 (2025-01-03)
diff --git a/src/marshmallow/base.py b/src/marshmallow/base.py
deleted file mode 100644
index fd05fccd..00000000
--- a/src/marshmallow/base.py
+++ /dev/null
@@ -1,33 +0,0 @@
-"""Abstract base classes.
-
-These are necessary to avoid circular imports between schema.py and fields.py.
-
-.. warning::
-
-    This module is treated as private API.
-    Users should not need to use this module directly.
-"""
-
-from __future__ import annotations
-
-from abc import ABC, abstractmethod
-
-
-class FieldABC(ABC):
-    """Abstract base class from which all Field classes inherit."""
-
-    @abstractmethod
-    def serialize(self, attr, obj, accessor=None):
-        pass
-
-    @abstractmethod
-    def deserialize(self, value):
-        pass
-
-    @abstractmethod
-    def _serialize(self, value, attr, obj, **kwargs):
-        pass
-
-    @abstractmethod
-    def _deserialize(self, value, attr, data, **kwargs):
-        pass
diff --git a/src/marshmallow/exceptions.py b/src/marshmallow/exceptions.py
index 096b6bd0..7554d479 100644
--- a/src/marshmallow/exceptions.py
+++ b/src/marshmallow/exceptions.py
@@ -67,5 +67,5 @@ class StringNotCollectionError(MarshmallowError, TypeError):
     """Raised when a string is passed when a list of strings is expected."""
 
 
-class FieldInstanceResolutionError(MarshmallowError, TypeError):
-    """Raised when schema to instantiate is neither a Schema class nor an instance."""
+class _FieldInstanceResolutionError(MarshmallowError, TypeError):
+    """Raised when an argument is passed to a field class that cannot be resolved to a Field instance."""
diff --git a/src/marshmallow/fields.py b/src/marshmallow/fields.py
index 76157f7c..985bd234 100644
--- a/src/marshmallow/fields.py
+++ b/src/marshmallow/fields.py
@@ -26,16 +26,14 @@ else:
     MonkeyPatch.patch_fromisoformat()
 
 from marshmallow import class_registry, types, utils, validate
-from marshmallow.base import FieldABC
 from marshmallow.exceptions import (
-    FieldInstanceResolutionError,
     StringNotCollectionError,
     ValidationError,
+    _FieldInstanceResolutionError,
 )
 from marshmallow.utils import (
     is_aware,
     is_collection,
-    resolve_field_instance,
 )
 from marshmallow.utils import (
     missing as missing_,
@@ -105,7 +103,22 @@ class _BaseFieldKwargs(typing.TypedDict, total=False):
     metadata: typing.Mapping[str, typing.Any] | None
 
 
-class Field(FieldABC):
+def _resolve_field_instance(cls_or_instance: Field | type[Field]) -> Field:
+    """Return a Field instance from a Field class or instance.
+
+    :param cls_or_instance: Field class or instance.
+    """
+    if isinstance(cls_or_instance, type):
+        if not issubclass(cls_or_instance, Field):
+            raise _FieldInstanceResolutionError
+        return cls_or_instance()
+    else:
+        if not isinstance(cls_or_instance, Field):
+            raise _FieldInstanceResolutionError
+        return cls_or_instance
+
+
+class Field:
     """Basic field from which other fields should extend. It applies no
     formatting by default, and should only be used in cases where
     data does not need to be formatted before being serialized or deserialized.
@@ -361,7 +374,7 @@ class Field(FieldABC):
         self.parent = self.parent or parent
         self.name = self.name or field_name
         self.root = self.root or (
-            self.parent.root if isinstance(self.parent, FieldABC) else self.parent
+            self.parent.root if isinstance(self.parent, Field) else self.parent
         )
 
     def _serialize(
@@ -689,11 +702,11 @@ class List(Field):
     ):
         super().__init__(**kwargs)
         try:
-            self.inner = resolve_field_instance(cls_or_instance)
-        except FieldInstanceResolutionError as error:
+            self.inner = _resolve_field_instance(cls_or_instance)
+        except _FieldInstanceResolutionError as error:
             raise ValueError(
                 "The list elements must be a subclass or instance of "
-                "marshmallow.base.FieldABC."
+                "marshmallow.fields.Field."
             ) from error
         if isinstance(self.inner, Nested):
             self.only = self.inner.only
@@ -764,13 +777,13 @@ class Tuple(Field):
 
         try:
             self.tuple_fields = [
-                resolve_field_instance(cls_or_instance)
+                _resolve_field_instance(cls_or_instance)
                 for cls_or_instance in tuple_fields
             ]
-        except FieldInstanceResolutionError as error:
+        except _FieldInstanceResolutionError as error:
             raise ValueError(
                 'Elements of "tuple_fields" must be subclasses or '
-                "instances of marshmallow.base.FieldABC."
+                "instances of marshmallow.fields.Field."
             ) from error
 
         self.validate_length = Length(equal=len(self.tuple_fields))
@@ -1510,22 +1523,22 @@ class Mapping(Field):
             self.key_field = None
         else:
             try:
-                self.key_field = resolve_field_instance(keys)
-            except FieldInstanceResolutionError as error:
+                self.key_field = _resolve_field_instance(keys)
+            except _FieldInstanceResolutionError as error:
                 raise ValueError(
                     '"keys" must be a subclass or instance of '
-                    "marshmallow.base.FieldABC."
+                    "marshmallow.fields.Field."
                 ) from error
 
         if values is None:
             self.value_field = None
         else:
             try:
-                self.value_field = resolve_field_instance(values)
-            except FieldInstanceResolutionError as error:
+                self.value_field = _resolve_field_instance(values)
+            except _FieldInstanceResolutionError as error:
                 raise ValueError(
                     '"values" must be a subclass or instance of '
-                    "marshmallow.base.FieldABC."
+                    "marshmallow.fields.Field."
                 ) from error
             if isinstance(self.value_field, Nested):
                 self.only = self.value_field.only
@@ -1844,11 +1857,11 @@ class Enum(Field):
                 self.field = Field()
             else:
                 try:
-                    self.field = resolve_field_instance(by_value)
-                except FieldInstanceResolutionError as error:
+                    self.field = _resolve_field_instance(by_value)
+                except _FieldInstanceResolutionError as error:
                     raise ValueError(
                         '"by_value" must be either a bool or a subclass or instance of '
-                        "marshmallow.base.FieldABC."
+                        "marshmallow.fields.Field."
                     ) from error
             self.choices_text = ", ".join(
                 str(self.field._serialize(m.value, None, None)) for m in enum
diff --git a/src/marshmallow/schema.py b/src/marshmallow/schema.py
index 111145e9..82031e00 100644
--- a/src/marshmallow/schema.py
+++ b/src/marshmallow/schema.py
@@ -13,7 +13,7 @@ from abc import ABCMeta
 from collections import defaultdict
 from collections.abc import Mapping
 
-from marshmallow import base, class_registry, types
+from marshmallow import class_registry, types
 from marshmallow import fields as ma_fields
 from marshmallow.decorators import (
     POST_DUMP,
@@ -47,7 +47,7 @@ def _get_fields(attrs):
     return [
         (field_name, field_value)
         for field_name, field_value in attrs.items()
-        if is_instance_or_subclass(field_value, base.FieldABC)
+        if is_instance_or_subclass(field_value, ma_fields.Field)
     ]
 
 
@@ -988,7 +988,7 @@ class Schema(metaclass=SchemaMeta):
             # Field declared as a class, not an instance. Ignore type checking because
             # we handle unsupported arg types, i.e. this is dead code from
             # the type checker's perspective.
-            if isinstance(field_obj, type) and issubclass(field_obj, base.FieldABC):
+            if isinstance(field_obj, type) and issubclass(field_obj, ma_fields.Field):
                 msg = (
                     f'Field for "{field_name}" must be declared as a '
                     "Field instance, not a class. "
diff --git a/src/marshmallow/utils.py b/src/marshmallow/utils.py
index b50fc029..70b556d2 100644
--- a/src/marshmallow/utils.py
+++ b/src/marshmallow/utils.py
@@ -9,9 +9,6 @@ import typing
 from collections.abc import Mapping
 from email.utils import format_datetime, parsedate_to_datetime
 
-from marshmallow.base import FieldABC
-from marshmallow.exceptions import FieldInstanceResolutionError
-
 EXCLUDE = "exclude"
 INCLUDE = "include"
 RAISE = "raise"
@@ -257,21 +254,6 @@ def get_func_args(func: typing.Callable) -> list[str]:
     return _signature(func)
 
 
-def resolve_field_instance(cls_or_instance):
-    """Return a Schema instance from a Schema class or instance.
-
-    :param type|Schema cls_or_instance: Marshmallow Schema class or instance.
-    """
-    if isinstance(cls_or_instance, type):
-        if not issubclass(cls_or_instance, FieldABC):
-            raise FieldInstanceResolutionError
-        return cls_or_instance()
-    else:
-        if not isinstance(cls_or_instance, FieldABC):
-            raise FieldInstanceResolutionError
-        return cls_or_instance
-
-
 def timedelta_to_microseconds(value: dt.timedelta) -> int:
     """Compute the total microseconds of a timedelta.
 


Test patch:
diff --git a/tests/test_serialization.py b/tests/test_serialization.py
index 60db524a..11d278ae 100644
--- a/tests/test_serialization.py
+++ b/tests/test_serialization.py
@@ -893,7 +893,7 @@ class TestFieldSerialization:
             fields.List("string")
         expected_msg = (
             "The list elements must be a subclass or instance of "
-            "marshmallow.base.FieldABC"
+            "marshmallow.fields.Field"
         )
         with pytest.raises(ValueError, match=expected_msg):
             fields.List(ASchema)
@@ -920,7 +920,7 @@ class TestFieldSerialization:
             fields.Tuple(fields.String)
         expected_msg = (
             'Elements of "tuple_fields" must be subclasses or '
-            "instances of marshmallow.base.FieldABC."
+            "instances of marshmallow.fields.Field."
         )
         with pytest.raises(ValueError, match=expected_msg):
             fields.Tuple([ASchema])