diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index d02eb12..3ba8020 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,9 +1,34 @@
 Changelog
 ---------
 
-8.3.1 (Unreleased)
+8.4.0 (Unreleased)
 ******************
 
+Features:
+
+* Add a new class attribute, ``empty_value`` to ``DelimitedList`` and
+  ``DelimitedTuple``, with a default of ``""``.
+  This controls the value deserialized when an empty string is seen.
+
+``empty_value`` can be used to handle types other than strings more gracefully, e.g.
+
+.. code-block:: python
+
+    from webargs import fields
+
+
+    class IntList(fields.DelimitedList):
+        empty_value = 0
+
+
+    myfield = IntList(fields.Int())
+
+.. note::
+
+    ``empty_value`` will be changing in webargs v9.0 to be ``missing`` by
+    default. This will allow use of fields with ``load_default`` to specify
+    handling of the empty value.
+
 Changes:
 
 * Type annotations for ``FlaskParser`` have been improved
diff --git a/src/webargs/fields.py b/src/webargs/fields.py
index 5598b54..96590a3 100644
--- a/src/webargs/fields.py
+++ b/src/webargs/fields.py
@@ -15,6 +15,8 @@ tells webargs where to parse the request argument from.
 """
 from __future__ import annotations
 
+import typing
+
 import marshmallow as ma
 
 # Expose all fields from marshmallow.fields.
@@ -64,6 +66,8 @@ class DelimitedFieldMixin:
     delimiter: str = ","
     # delimited fields set is_multiple=False for webargs.core.is_multiple
     is_multiple: bool = False
+    # NOTE: in 8.x this defaults to "" but in 9.x it will be 'missing'
+    empty_value: typing.Any = ""
 
     def _serialize(self, value, attr, obj, **kwargs):
         # serializing will start with parent-class serialization, so that we correctly
@@ -77,6 +81,8 @@ class DelimitedFieldMixin:
         if not isinstance(value, (str, bytes)):
             raise self.make_error("invalid")
         values = value.split(self.delimiter) if value else []
+        # convert empty strings to the empty value; typically "" and therefore a no-op
+        values = [v or self.empty_value for v in values]
         return super()._deserialize(values, attr, data, **kwargs)
 
 
@@ -117,6 +123,12 @@ class DelimitedTuple(DelimitedFieldMixin, ma.fields.Tuple):
 
     default_error_messages = {"invalid": "Not a valid delimited tuple."}
 
-    def __init__(self, tuple_fields, *, delimiter: str | None = None, **kwargs):
+    def __init__(
+        self,
+        tuple_fields,
+        *,
+        delimiter: str | None = None,
+        **kwargs,
+    ):
         self.delimiter = delimiter or self.delimiter
         super().__init__(tuple_fields, **kwargs)

diff --git a/tests/test_core.py b/tests/test_core.py
index c24ac1b..39b8284 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -11,6 +11,7 @@ from marshmallow import (
     INCLUDE,
     RAISE,
     Schema,
+    missing,
     post_load,
     pre_load,
     validates_schema,
@@ -1106,6 +1107,47 @@ def test_delimited_tuple_passed_invalid_type(web_request, parser):
     assert excinfo.value.messages == {"json": {"ids": ["Not a valid delimited tuple."]}}
 
 
+def test_delimited_list_custom_empty_value(web_request, parser):
+    class ZeroList(fields.DelimitedList):
+        empty_value = 0
+
+    web_request.json = {"ids": "1,,3"}
+    schema_cls = Schema.from_dict({"ids": ZeroList(fields.Int())})
+    schema = schema_cls()
+
+    parsed = parser.parse(schema, web_request)
+    assert parsed["ids"] == [1, 0, 3]
+
+
+def test_delimited_tuple_custom_empty_value(web_request, parser):
+    class ZeroTuple(fields.DelimitedTuple):
+        empty_value = 0
+
+    web_request.json = {"ids": "1,,3"}
+    schema_cls = Schema.from_dict(
+        {"ids": ZeroTuple((fields.Int, fields.Int, fields.Int))}
+    )
+    schema = schema_cls()
+
+    parsed = parser.parse(schema, web_request)
+    assert parsed["ids"] == (1, 0, 3)
+
+
+def test_delimited_list_using_missing_for_empty(web_request, parser):
+    # this is "future" because we plan to make this the default for webargs v9.0
+    class FutureList(fields.DelimitedList):
+        empty_value = missing
+
+    web_request.json = {"ids": "foo,,bar"}
+    schema_cls = Schema.from_dict(
+        {"ids": FutureList(fields.String(load_default="nil"))}
+    )
+    schema = schema_cls()
+
+    parsed = parser.parse(schema, web_request)
+    assert parsed["ids"] == ["foo", "nil", "bar"]
+
+
 def test_missing_list_argument_not_in_parsed_result(web_request, parser):
     # arg missing in request
     web_request.json = {}

Dealing with empty values in `DelimitedFieldMixin`
`DelimitedList(String())` deserializes "a,,c" as `["a", "", "c"]`.

I guess this meets user expectations.

My expectation with integers would be that

`DelimitedList(Integer(allow_none=True))` deserializes `"1,,3"` as `[1,None,3]`

but it errors.

The reason for this is that when the string is split, it is turned into `["1", "", "3"]`. This is why it works in the string case.

I'm not sure this was really intended. It may be a side effect of the `split` function that happens to do well with strings.

We could change that to replace empty values with `None`. But it would break the string use case, unless the user overloads `String` field to deserialize `None` as `""`.

Likewise, users may already overload `Integer` to deserialize `""` as `None` and no modification is required to `DelimitedFieldMixin`.

Just been caught by this and wondering out loud. Advice welcome.

In any case, there is an intrinsic limitation in the delimited string format: one can't distinguish empty string from missing value (as opposed to a JSON payload). It is not clear to me how OpenAPI (for instance) deals with the case of an empty element in an array (in a query argument).
tests/test_core.py::test_delimited_list_custom_empty_value tests/test_core.py::test_delimited_tuple_custom_empty_value tests/test_core.py::test_delimited_list_using_missing_for_empty
tests/test_core.py::test_load_json_called_by_parse_default tests/test_core.py::test_load_nondefault_called_by_parse_with_location[querystring] tests/test_core.py::test_load_nondefault_called_by_parse_with_location[form] tests/test_core.py::test_load_nondefault_called_by_parse_with_location[headers] tests/test_core.py::test_load_nondefault_called_by_parse_with_location[cookies] tests/test_core.py::test_load_nondefault_called_by_parse_with_location[files] tests/test_core.py::test_parse[parse] tests/test_core.py::test_parse[async_parse] tests/test_core.py::test_parse_with_unknown_behavior_specified[schema_instance] tests/test_core.py::test_parse_with_unknown_behavior_specified[parse_call] tests/test_core.py::test_parse_with_unknown_behavior_specified[parser_default] tests/test_core.py::test_parse_with_unknown_behavior_specified[parser_class_default] tests/test_core.py::test_parse_with_explicit_unknown_overrides_schema tests/test_core.py::test_parse_with_default_unknown_cleared_uses_schema_value[custom_class] tests/test_core.py::test_parse_with_default_unknown_cleared_uses_schema_value[instance_setting] tests/test_core.py::test_parse_with_default_unknown_cleared_uses_schema_value[both] tests/test_core.py::test_parse_required_arg_raises_validation_error[parse] tests/test_core.py::test_parse_required_arg_raises_validation_error[async_parse] tests/test_core.py::test_arg_not_required_excluded_in_parsed_output tests/test_core.py::test_arg_allow_none tests/test_core.py::test_parse_required_arg tests/test_core.py::test_parse_required_list tests/test_core.py::test_parse_list_allow_none tests/test_core.py::test_parse_list_dont_allow_none tests/test_core.py::test_parse_empty_list tests/test_core.py::test_parse_missing_list tests/test_core.py::test_default_location tests/test_core.py::test_missing_with_default tests/test_core.py::test_default_can_be_none tests/test_core.py::test_arg_with_default_and_location tests/test_core.py::test_value_error_raised_if_parse_called_with_invalid_location tests/test_core.py::test_handle_error_called_when_parsing_raises_error tests/test_core.py::test_handle_error_called_when_async_parsing_raises_error tests/test_core.py::test_handle_error_reraises_errors tests/test_core.py::test_location_as_init_argument tests/test_core.py::test_custom_error_handler tests/test_core.py::test_custom_error_handler_decorator tests/test_core.py::test_custom_error_handler_decorator_in_async_parse[True] tests/test_core.py::test_custom_error_handler_decorator_in_async_parse[False] tests/test_core.py::test_custom_error_handler_must_reraise tests/test_core.py::test_custom_location_loader tests/test_core.py::test_custom_location_loader_with_data_key tests/test_core.py::test_full_input_validation tests/test_core.py::test_full_input_validation_with_multiple_validators tests/test_core.py::test_required_with_custom_error tests/test_core.py::test_required_with_custom_error_and_validation_error tests/test_core.py::test_full_input_validator_receives_nonascii_input tests/test_core.py::test_invalid_argument_for_validate tests/test_core.py::test_multidict_proxy[input_dict0] tests/test_core.py::test_multidict_proxy[input_dict1] tests/test_core.py::test_multidict_proxy[input_dict2] tests/test_core.py::test_parse_with_data_key tests/test_core.py::test_parse_with_data_key_retains_field_name_in_error tests/test_core.py::test_parse_nested_with_data_key tests/test_core.py::test_parse_nested_with_missing_key_and_data_key tests/test_core.py::test_parse_nested_with_default tests/test_core.py::test_nested_many tests/test_core.py::test_use_args tests/test_core.py::test_use_args_stacked tests/test_core.py::test_use_args_forbids_invalid_usages tests/test_core.py::test_use_kwargs_stacked tests/test_core.py::test_decorators_dont_change_docstring[use_args] tests/test_core.py::test_decorators_dont_change_docstring[use_kwargs] tests/test_core.py::test_list_allowed_missing tests/test_core.py::test_int_list_allowed_missing tests/test_core.py::test_multiple_arg_required_with_int_conversion tests/test_core.py::test_parse_with_callable tests/test_core.py::test_use_args_callable tests/test_core.py::TestPassingSchema::test_passing_schema_to_parse tests/test_core.py::TestPassingSchema::test_use_args_can_be_passed_a_schema tests/test_core.py::TestPassingSchema::test_passing_schema_factory_to_parse tests/test_core.py::TestPassingSchema::test_use_args_can_be_passed_a_schema_factory tests/test_core.py::TestPassingSchema::test_use_kwargs_can_be_passed_a_schema tests/test_core.py::TestPassingSchema::test_use_kwargs_can_be_passed_a_schema_factory tests/test_core.py::TestPassingSchema::test_use_kwargs_stacked tests/test_core.py::TestPassingSchema::test_parse_does_not_add_missing_values_to_schema_validator tests/test_core.py::test_use_args_with_custom_location_in_parser tests/test_core.py::test_use_kwargs tests/test_core.py::test_use_kwargs_with_arg_missing tests/test_core.py::test_delimited_list_empty_string tests/test_core.py::test_delimited_list_default_delimiter tests/test_core.py::test_delimited_tuple_default_delimiter tests/test_core.py::test_delimited_tuple_incorrect_arity tests/test_core.py::test_delimited_list_with_datetime tests/test_core.py::test_delimited_list_custom_delimiter tests/test_core.py::test_delimited_tuple_custom_delimiter tests/test_core.py::test_delimited_list_load_list_errors tests/test_core.py::test_delimited_tuple_load_list_errors tests/test_core.py::test_delimited_list_passed_invalid_type tests/test_core.py::test_delimited_tuple_passed_invalid_type tests/test_core.py::test_missing_list_argument_not_in_parsed_result tests/test_core.py::test_type_conversion_with_multiple_required tests/test_core.py::test_is_multiple_detection[is_multiple_true-input_dict0] tests/test_core.py::test_is_multiple_detection[is_multiple_true-input_dict1] tests/test_core.py::test_is_multiple_detection[is_multiple_true-input_dict2] tests/test_core.py::test_is_multiple_detection[is_multiple_false-input_dict0] tests/test_core.py::test_is_multiple_detection[is_multiple_false-input_dict1] tests/test_core.py::test_is_multiple_detection[is_multiple_false-input_dict2] tests/test_core.py::test_is_multiple_detection[is_multiple_notset-input_dict0] tests/test_core.py::test_is_multiple_detection[is_multiple_notset-input_dict1] tests/test_core.py::test_is_multiple_detection[is_multiple_notset-input_dict2] tests/test_core.py::test_is_multiple_detection[list_field-input_dict0] tests/test_core.py::test_is_multiple_detection[list_field-input_dict1] tests/test_core.py::test_is_multiple_detection[list_field-input_dict2] tests/test_core.py::test_is_multiple_detection[tuple_field-input_dict0] tests/test_core.py::test_is_multiple_detection[tuple_field-input_dict1] tests/test_core.py::test_is_multiple_detection[tuple_field-input_dict2] tests/test_core.py::test_is_multiple_detection[added_to_known-input_dict0] tests/test_core.py::test_is_multiple_detection[added_to_known-input_dict1] tests/test_core.py::test_is_multiple_detection[added_to_known-input_dict2] tests/test_core.py::test_validation_errors_in_validator_are_passed_to_handle_error tests/test_core.py::test_parse_basic tests/test_core.py::test_parse_raises_validation_error_if_data_invalid tests/test_core.py::test_nested_field_from_dict tests/test_core.py::test_is_json tests/test_core.py::test_get_mimetype tests/test_core.py::test_parse_with_error_status_code_and_headers tests/test_core.py::test_custom_schema_class tests/test_core.py::test_custom_default_schema_class tests/test_core.py::test_parser_pre_load tests/test_core.py::test_whitespace_stripping_parser_example tests/test_core.py::test_parse_allows_non_dict_argmap_mapping tests/test_core.py::test_use_args_allows_non_dict_argmap_mapping tests/test_core.py::test_parse_rejects_unknown_argmap_type tests/test_core.py::test_parser_opt_out_positional_args tests/test_core.py::test_use_args_implicit_arg_names tests/test_core.py::test_use_args_explicit_arg_names[True] tests/test_core.py::test_use_args_explicit_arg_names[False] tests/test_core.py::test_use_args_errors_on_explicit_arg_name_conflict tests/test_core.py::test_use_args_errors_on_implicit_arg_name_conflict tests/test_core.py::test_use_args_with_arg_name_supports_multi_stacked_decorators