sirosen: > I suppose you added a new commit to help me reviewing but you'd like to squash before merging.

Yep! I'm happy to squash things together now. I just figured the added diff would make it easier.

> I can do the squash / update changelog / release later when I get the time.

I'm also happy to do this. As a relatively new contributor, I don't want to be too pushy about my changes or doing releases -- let me know if I'm being too tentative.

I'll do a rebase and add a changelog entry, so this should simply be a merge + tweak to the changelog (to add the release date) when the time comes.
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 1e1bfda..d5e3c65 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,17 @@
 Changelog
 ---------
 
+6.1.0 (Unreleased)
+******************
+
+Features:
+
+* Add ``fields.DelimitedTuple`` when using marshmallow 3. This behaves as a
+  combination of ``fields.DelimitedList`` and ``marshmallow.fields.Tuple``. It
+  takes an iterable of fields, plus a delimiter (defaults to ``,``), and parses
+  delimiter-separated strings into tuples.
+
+
 6.0.0 (2020-02-27)
 ******************
 
diff --git a/docs/upgrading.rst b/docs/upgrading.rst
index 9879692..147759f 100644
--- a/docs/upgrading.rst
+++ b/docs/upgrading.rst
@@ -11,15 +11,15 @@ Multiple Locations Are No Longer Supported In A Single Call
 
 The default location is JSON/body.
 
-Under webargs 5.x, code often did not have to specify a location.
+Under webargs 5.x, you may have written code which did not specify a location.
 
-Because webargs would parse data from multiple locations automatically, users
-did not need to specify where a parameter, call it `q`, was passed.
+Because webargs would parse data from multiple locations automatically, you did
+not need to specify where a parameter, call it `q`, was passed.
 `q` could be in a query parameter or in a JSON or form-post body.
 
-Now, webargs requires that users specify only one location for data loading per
+Now, webargs requires that you specify only one location for data loading per
 `use_args` call, and `"json"` is the default. If `q` is intended to be a query
-parameter, the developer must be explicit and rewrite like so:
+parameter, you must be explicit and rewrite like so:
 
 .. code-block:: python
 
@@ -34,9 +34,9 @@ parameter, the developer must be explicit and rewrite like so:
     def foo(args):
         return some_function(user_query=args.get("q"))
 
-This also means that another usage from 5.x is not supported. Code with
-multiple locations in a single `use_args`, `use_kwargs`, or `parse` call
-must be rewritten in multiple separate `use_args` or `use_kwargs` invocations,
+This also means that another usage from 5.x is not supported. If you had code
+with multiple locations in a single `use_args`, `use_kwargs`, or `parse` call,
+you must write it in multiple separate `use_args` or `use_kwargs` invocations,
 like so:
 
 .. code-block:: python
@@ -87,11 +87,9 @@ location_handler Has Been Replaced With location_loader
 This is not just a name change. The expected signature of a `location_loader`
 is slightly different from the signature for a `location_handler`.
 
-Where previously a `location_handler` code took the incoming request data and
-details of a single field being loaded, a `location_loader` takes the request
-and the schema as a pair. It does not return a specific field's data, but data
-for the whole location.
-
+Where previously your code took the incoming request data and details of a
+single field being loaded, it now takes the request and the schema as a pair.
+It does not return a specific field's data, but data for the whole location.
 Rewrite code like this:
 
 .. code-block:: python
@@ -107,8 +105,8 @@ Rewrite code like this:
     def load_data(request, schema):
         return request.data
 
-Data Is Not Filtered Before Being Passed To Schemas, And It May Be Proxified
-----------------------------------------------------------------------------
+Data Is Not Filtered Before Being Passed To Your Schema, And It May Be Proxified
+--------------------------------------------------------------------------------
 
 In webargs 5.x, the deserialization schema was used to pull data out of the
 request object. That data was compiled into a dictionary which was then passed
@@ -119,8 +117,16 @@ on schemas. This lets a schema decide what to do with fields not specified in
 the schema. In order to achieve this, webargs now passes the full data from
 the specified location to the schema.
 
-Therefore, users should specify `unknown=marshmallow.EXCLUDE` on their schemas in
-order to filter out unknown fields. Like so:
+However, many types of request data are so-called "multidicts" -- dictionary-like
+types which can return one or multiple values as you prefer. To handle
+`marshmallow.fields.List` and `webargs.fields.DelimitedList` fields correctly,
+passing list data, webargs must combine schema information with the raw request
+data. This is done in the
+:class:`MultiDictProxy <webargs.multidictproxy.MultiDictProxy>` type, which
+will often be passed to your schemas.
+
+Therefore, you should specify `unknown=marshmallow.EXCLUDE` on your schemas if
+you want to filter out unknown fields. Like so:
 
 .. code-block:: python
 
@@ -155,7 +161,8 @@ order to filter out unknown fields. Like so:
         ...
 
 
-This also allows usage which passes the unknown parameters through, like so:
+This also allows you to write code which passes the unknown parameters through,
+like so:
 
 .. code-block:: python
 
@@ -170,17 +177,11 @@ This also allows usage which passes the unknown parameters through, like so:
         ...
 
 
-However, many types of request data are so-called "multidicts" -- dictionary-like
-types which can return one or multiple values. To handle `marshmallow.fields.List`
-and `webargs.fields.DelimitedList` fields correctly, passing list data, webargs
-must combine schema information with the raw request data. This is done in the
-:class:`MultiDictProxy <webargs.multidictproxy.MultiDictProxy>` type, which
-will often be passed to schemas.
-
-This means that if a schema has a `pre_load` hook which interacts with the data,
+Finally, this change passes a proxy object where you once saw a dict. This
+means that if your schema has a `pre_load` hook which interacts with the data,
 it may need modifications. For example, a `flask` query string will be parsed
 into an `ImmutableMultiDict` type, which will break pre-load hooks which modify
-the data in-place. Such usages need rewrites like so:
+the data in-place. You may need to apply rewrites like so:
 
 .. code-block:: python
 
@@ -230,8 +231,8 @@ DelimitedList Now Only Takes A String Input
 -------------------------------------------
 
 Combining `List` and string parsing functionality in a single type had some
-messy corner cases. For the most part, this should not require rewrites. But
-for APIs which need to allow both usages, rewrites are possible like so:
+messy corner cases. For the most part, this should not require rewrites. But if
+you need to allow both usages in your API, you can do a rewrite like so:
 
 .. code-block:: python
 
@@ -273,14 +274,14 @@ for APIs which need to allow both usages, rewrites are possible like so:
 ValidationError Messages Are Namespaced Under The Location
 ----------------------------------------------------------
 
-Code parsing ValidationError messages will notice a change in the messages
-produced by webargs.
+If you were parsing ValidationError messages, you will notice a change in the
+messages produced by webargs.
 What would previously have come back with messages like `{"foo":["Not a valid integer."]}`
 will now have messages nested one layer deeper, like
 `{"json":{"foo":["Not a valid integer."]}}`.
 
-To rewrite code which was handling these errors, the handler will need to be
-prepared to traverse messages by one additional level. For example:
+To rewrite code which was handling these errors, you will need to be prepared
+to traverse messages by one additional level. For example:
 
 .. code-block:: python
 
diff --git a/src/webargs/fields.py b/src/webargs/fields.py
index 851756b..2227f3f 100644
--- a/src/webargs/fields.py
+++ b/src/webargs/fields.py
@@ -43,26 +43,24 @@ class Nested(ma.fields.Nested):
         super().__init__(nested, *args, **kwargs)
 
 
-class DelimitedList(ma.fields.List):
-    """A field which is similar to a List, but takes its input as a delimited
-    string (e.g. "foo,bar,baz").
+class DelimitedFieldMixin:
+    """
+    This is a mixin class for subclasses of ma.fields.List and ma.fields.Tuple
+    which split on a pre-specified delimiter. By default, the delimiter will be ","
 
-    Like List, it can be given a nested field type which it will use to
-    de/serialize each element of the list.
+    Because we want the MRO to reach this class before the List or Tuple class,
+    it must be listed first in the superclasses
 
-    :param Field cls_or_instance: A field class or instance.
-    :param str delimiter: Delimiter between values.
+    For example, a DelimitedList-like type can be defined like so:
+
+    >>> class MyDelimitedList(DelimitedFieldMixin, ma.fields.List):
+    >>>     pass
     """
 
-    default_error_messages = {"invalid": "Not a valid delimited list."}
     delimiter = ","
 
-    def __init__(self, cls_or_instance, *, delimiter=None, **kwargs):
-        self.delimiter = delimiter or self.delimiter
-        super().__init__(cls_or_instance, **kwargs)
-
     def _serialize(self, value, attr, obj):
-        # serializing will start with List serialization, so that we correctly
+        # serializing will start with parent-class serialization, so that we correctly
         # output lists of non-primitive types, e.g. DelimitedList(DateTime)
         return self.delimiter.join(
             format(each) for each in super()._serialize(value, attr, obj)
@@ -76,3 +74,45 @@ class DelimitedList(ma.fields.List):
             else:
                 raise self.make_error("invalid")
         return super()._deserialize(value.split(self.delimiter), attr, data, **kwargs)
+
+
+class DelimitedList(DelimitedFieldMixin, ma.fields.List):
+    """A field which is similar to a List, but takes its input as a delimited
+    string (e.g. "foo,bar,baz").
+
+    Like List, it can be given a nested field type which it will use to
+    de/serialize each element of the list.
+
+    :param Field cls_or_instance: A field class or instance.
+    :param str delimiter: Delimiter between values.
+    """
+
+    default_error_messages = {"invalid": "Not a valid delimited list."}
+    delimiter = ","
+
+    def __init__(self, cls_or_instance, *, delimiter=None, **kwargs):
+        self.delimiter = delimiter or self.delimiter
+        super().__init__(cls_or_instance, **kwargs)
+
+
+# DelimitedTuple can only be defined when using marshmallow3, when Tuple was
+# added
+if MARSHMALLOW_VERSION_INFO[0] >= 3:
+
+    class DelimitedTuple(DelimitedFieldMixin, ma.fields.Tuple):
+        """A field which is similar to a Tuple, but takes its input as a delimited
+        string (e.g. "foo,bar,baz").
+
+        Like Tuple, it can be given a tuple of nested field types which it will use to
+        de/serialize each element of the tuple.
+
+        :param Iterable[Field] tuple_fields: An iterable of field classes or instances.
+        :param str delimiter: Delimiter between values.
+        """
+
+        default_error_messages = {"invalid": "Not a valid delimited tuple."}
+        delimiter = ","
+
+        def __init__(self, tuple_fields, *, delimiter=None, **kwargs):
+            self.delimiter = delimiter or self.delimiter
+            super().__init__(tuple_fields, **kwargs)

'Not a valid tuple.'  when trying to use marshmallow fields.Tuple for argument validation
I'm trying to use the marshmallow fields.Tuple for querystring argument validation on a GET request using Flask.  The issue I'm running into is that no matter what type of object I declare and no matter what I use in the request, I always get the default 'Not a valid tuple.' response.  I have tried using a tuple of size 1 and 2; using fields.String and/or fields.Integer, etc with the same result.

- I'm using Python 3.6.9 with these dependencies:
anyjson==0.3.3
apipkg==1.5
arrow==0.15.5
attrs==19.3.0
backports.functools-lru-cache==1.6.1
cassandra-driver==3.22.0
Cerberus==1.3.2
certifi==2019.11.28
cffi==1.13.2
chardet==3.0.4
click==7.1.1
execnet==1.7.1
Flask==1.1.1
Flask-Cors==3.0.8
funcsigs==1.0.2
futures==3.1.1
geomet==0.1.2
gevent==1.4.0
greenlet==0.4.13
gunicorn==20.0.4
idna==2.9
importlib-metadata==1.6.0
itsdangerous==1.1.0
Jinja2==2.11.1
jsonklog==0.15.0
MarkupSafe==1.1.1
marshmallow==3.5.1
neurolab==0.3.5
numpy==1.18.1
pluggy==0.13.1
py==1.8.1
pyaml==20.3.1
pymongo==3.10.1
pytest==3.3.0
pytest-forked==0.2
pytest-xdist==1.20.1
python-dateutil==2.8.1
PyYAML==5.3.1
readline==6.2.4.1
requests==2.23.0
six==1.14.0
urllib3==1.25.8
webargs==6.0.0
Werkzeug==1.0.0
zipp==3.1.0

- Here is an example of what I'm trying to do:
```
from flask import Flask
from webargs.flaskparser import parser, use_kwargs
from marshmallow import EXCLUDE, fields, Schema


app = Flask(__name__)


@app.errorhandler(422)
def custom_handler(error):
    errors = []
    if 'query' in error.data['messages']:
        for arg in error.data['messages']['query']:
            for item in error.data['messages']['query'][arg]:
                errors.append(item)
    return str(errors), 400


class test_schema(Schema):
    class Meta:
        unknown = EXCLUDE
        strict = True
    
    test_tup = fields.Tuple((fields.Integer(required=True), fields.Integer(required=True)), required=True)


@app.route('/test/', strict_slashes=False)
@parser.use_kwargs(test_schema, location='query')
def test_the_mallow(**kwargs):
    return "True"
```

- Finally, here are a couple example url's I've tried:
  localhost:2300/test/?test_tup=[0,0]
  localhost:2300/test/?test_tup=(0,0)
  localhost:2300/test/?test_tup=0,0

diff --git a/tests/test_core.py b/tests/test_core.py
index fef3c65..a9a32e9 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -851,7 +851,53 @@ def test_delimited_list_default_delimiter(web_request, parser):
     assert data["ids"] == "1,2,3"
 
 
-def test_delimited_list_as_string_v2(web_request, parser):
+@pytest.mark.skipif(
+    MARSHMALLOW_VERSION_INFO[0] < 3, reason="fields.Tuple added in marshmallow3"
+)
+def test_delimited_tuple_default_delimiter(web_request, parser):
+    """
+    Test load and dump from DelimitedTuple, including the use of a datetime
+    type (similar to a DelimitedList test below) which confirms that we aren't
+    relying on __str__, but are properly de/serializing the included fields
+    """
+    web_request.json = {"ids": "1,2,2020-05-04"}
+    schema_cls = dict2schema(
+        {
+            "ids": fields.DelimitedTuple(
+                (fields.Int, fields.Int, fields.DateTime(format="%Y-%m-%d"))
+            )
+        }
+    )
+    schema = schema_cls()
+
+    parsed = parser.parse(schema, web_request)
+    assert parsed["ids"] == (1, 2, datetime.datetime(2020, 5, 4))
+
+    data = schema.dump(parsed)
+    assert data["ids"] == "1,2,2020-05-04"
+
+
+@pytest.mark.skipif(
+    MARSHMALLOW_VERSION_INFO[0] < 3, reason="fields.Tuple added in marshmallow3"
+)
+def test_delimited_tuple_incorrect_arity(web_request, parser):
+    web_request.json = {"ids": "1,2"}
+    schema_cls = dict2schema(
+        {"ids": fields.DelimitedTuple((fields.Int, fields.Int, fields.Int))}
+    )
+    schema = schema_cls()
+
+    with pytest.raises(ValidationError):
+        parser.parse(schema, web_request)
+
+
+def test_delimited_list_with_datetime(web_request, parser):
+    """
+    Test that DelimitedList(DateTime(format=...)) correctly parses and dumps
+    dates to and from strings -- indicates that we're doing proper
+    serialization of values in dump() and not just relying on __str__ producing
+    correct results
+    """
     web_request.json = {"dates": "2018-11-01,2018-11-02"}
     schema_cls = dict2schema(
         {"dates": fields.DelimitedList(fields.DateTime(format="%Y-%m-%d"))}
@@ -877,6 +923,27 @@ def test_delimited_list_custom_delimiter(web_request, parser):
     parsed = parser.parse(schema, web_request)
     assert parsed["ids"] == [1, 2, 3]
 
+    dumped = schema.dump(parsed)
+    data = dumped.data if MARSHMALLOW_VERSION_INFO[0] < 3 else dumped
+    assert data["ids"] == "1|2|3"
+
+
+@pytest.mark.skipif(
+    MARSHMALLOW_VERSION_INFO[0] < 3, reason="fields.Tuple added in marshmallow3"
+)
+def test_delimited_tuple_custom_delimiter(web_request, parser):
+    web_request.json = {"ids": "1|2"}
+    schema_cls = dict2schema(
+        {"ids": fields.DelimitedTuple((fields.Int, fields.Int), delimiter="|")}
+    )
+    schema = schema_cls()
+
+    parsed = parser.parse(schema, web_request)
+    assert parsed["ids"] == (1, 2)
+
+    data = schema.dump(parsed)
+    assert data["ids"] == "1|2"
+
 
 def test_delimited_list_load_list_errors(web_request, parser):
     web_request.json = {"ids": [1, 2, 3]}
@@ -891,6 +958,22 @@ def test_delimited_list_load_list_errors(web_request, parser):
     assert errors["ids"] == ["Not a valid delimited list."]
 
 
+@pytest.mark.skipif(
+    MARSHMALLOW_VERSION_INFO[0] < 3, reason="fields.Tuple added in marshmallow3"
+)
+def test_delimited_tuple_load_list_errors(web_request, parser):
+    web_request.json = {"ids": [1, 2]}
+    schema_cls = dict2schema({"ids": fields.DelimitedTuple((fields.Int, fields.Int))})
+    schema = schema_cls()
+
+    with pytest.raises(ValidationError) as excinfo:
+        parser.parse(schema, web_request)
+    exc = excinfo.value
+    assert isinstance(exc, ValidationError)
+    errors = exc.args[0]
+    assert errors["ids"] == ["Not a valid delimited tuple."]
+
+
 # Regresion test for https://github.com/marshmallow-code/webargs/issues/149
 def test_delimited_list_passed_invalid_type(web_request, parser):
     web_request.json = {"ids": 1}
@@ -902,6 +985,19 @@ def test_delimited_list_passed_invalid_type(web_request, parser):
     assert excinfo.value.messages == {"json": {"ids": ["Not a valid delimited list."]}}
 
 
+@pytest.mark.skipif(
+    MARSHMALLOW_VERSION_INFO[0] < 3, reason="fields.Tuple added in marshmallow3"
+)
+def test_delimited_tuple_passed_invalid_type(web_request, parser):
+    web_request.json = {"ids": 1}
+    schema_cls = dict2schema({"ids": fields.DelimitedTuple((fields.Int,))})
+    schema = schema_cls()
+
+    with pytest.raises(ValidationError) as excinfo:
+        parser.parse(schema, web_request)
+    assert excinfo.value.messages == {"json": {"ids": ["Not a valid delimited tuple."]}}
+
+
 def test_missing_list_argument_not_in_parsed_result(web_request, parser):
     # arg missing in request
     web_request.json = {}

pip install -e .[dev]
parse_log_pytest
pytest
pytest pre-commit
apt-get update apt-get install -y gcc
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
aiohappyeyeballs==2.6.1
aiohttp==3.11.14
aiosignal==1.3.2
asgiref==3.8.1
async-timeout==5.0.1
attrs==25.3.0
beautifulsoup4==4.13.3
blinker==1.9.0
bottle==0.13.2
cachetools==5.5.2
cfgv==3.4.0
chardet==5.2.0
click==8.1.8
colorama==0.4.6
distlib==0.3.9
Django==4.2.20
entrypoints==0.3
exceptiongroup @ file:///croot/exceptiongroup_1706031385326/work
falcon==4.0.2
filelock==3.18.0
flake8==3.7.9
flake8-bugbear==20.1.4
Flask==3.1.0
frozenlist==1.5.0
hupper==1.12.1
identify==2.6.9
idna==3.10
importlib_metadata==8.6.1
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
itsdangerous==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.2
marshmallow==3.26.1
mccabe==0.6.1
multidict==6.2.0
mypy==0.770
mypy_extensions==0.4.4
nodeenv==1.9.1
packaging @ file:///croot/packaging_1734472117206/work
PasteDeploy==3.1.0
plaster==1.1.2
plaster-pastedeploy==1.0.1
platformdirs==4.3.7
pluggy @ file:///croot/pluggy_1733169602837/work
pre-commit==2.21.0
propcache==0.3.1
pycodestyle==2.5.0
pyflakes==2.1.1
pyproject-api==1.9.0
pyramid==2.0.2
pytest @ file:///croot/pytest_1738938843180/work
pytest-aiohttp==1.1.0
pytest-asyncio==0.26.0
PyYAML==6.0.2
six==1.17.0
soupsieve==2.6
sqlparse==0.5.3
tomli==2.2.1
tornado==6.4.2
tox==4.25.0
translationstring==1.4
typed-ast==1.4.3
typing_extensions==4.13.0
venusian==3.1.1
virtualenv==20.29.3
waitress==3.0.2
webapp2==3.0.0b1
-e git+https://github.com/marshmallow-code/webargs.git@2c85a334ea59095f17a0c7fb6e6617adbf356e84#egg=webargs
WebOb==1.8.9
WebTest==2.0.35
webtest-aiohttp==2.0.0
Werkzeug==3.1.3
yarl==1.18.3
zipp==3.21.0
zope.deprecation==5.1
zope.interface==7.2

name: webargs
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - exceptiongroup=1.2.0=py39h06a4308_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - packaging=24.2=py39h06a4308_0
  - pip=25.0=py39h06a4308_0
  - pluggy=1.5.0=py39h06a4308_0
  - pytest=8.3.4=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - aiohappyeyeballs==2.6.1
      - aiohttp==3.11.14
      - aiosignal==1.3.2
      - asgiref==3.8.1
      - async-timeout==5.0.1
      - attrs==25.3.0
      - beautifulsoup4==4.13.3
      - blinker==1.9.0
      - bottle==0.13.2
      - cachetools==5.5.2
      - cfgv==3.4.0
      - chardet==5.2.0
      - click==8.1.8
      - colorama==0.4.6
      - distlib==0.3.9
      - django==4.2.20
      - entrypoints==0.3
      - falcon==4.0.2
      - filelock==3.18.0
      - flake8==3.7.9
      - flake8-bugbear==20.1.4
      - flask==3.1.0
      - frozenlist==1.5.0
      - hupper==1.12.1
      - identify==2.6.9
      - idna==3.10
      - importlib-metadata==8.6.1
      - itsdangerous==2.2.0
      - jinja2==3.1.6
      - markupsafe==3.0.2
      - marshmallow==3.26.1
      - mccabe==0.6.1
      - multidict==6.2.0
      - mypy==0.770
      - mypy-extensions==0.4.4
      - nodeenv==1.9.1
      - pastedeploy==3.1.0
      - plaster==1.1.2
      - plaster-pastedeploy==1.0.1
      - platformdirs==4.3.7
      - pre-commit==2.21.0
      - propcache==0.3.1
      - pycodestyle==2.5.0
      - pyflakes==2.1.1
      - pyproject-api==1.9.0
      - pyramid==2.0.2
      - pytest-aiohttp==1.1.0
      - pytest-asyncio==0.26.0
      - pyyaml==6.0.2
      - six==1.17.0
      - soupsieve==2.6
      - sqlparse==0.5.3
      - tomli==2.2.1
      - tornado==6.4.2
      - tox==4.25.0
      - translationstring==1.4
      - typed-ast==1.4.3
      - typing-extensions==4.13.0
      - venusian==3.1.1
      - virtualenv==20.29.3
      - waitress==3.0.2
      - webapp2==3.0.0b1
      - webob==1.8.9
      - webtest==2.0.35
      - webtest-aiohttp==2.0.0
      - werkzeug==3.1.3
      - yarl==1.18.3
      - zipp==3.21.0
      - zope-deprecation==5.1
      - zope-interface==7.2
prefix: /opt/conda/envs/webargs

tests/test_core.py::test_delimited_tuple_default_delimiter tests/test_core.py::test_delimited_tuple_incorrect_arity tests/test_core.py::test_delimited_tuple_custom_delimiter tests/test_core.py::test_delimited_tuple_load_list_errors tests/test_core.py::test_delimited_tuple_passed_invalid_type
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 tests/test_core.py::test_parse_with_unknown_behavior_specified tests/test_core.py::test_parse_required_arg_raises_validation_error 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_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_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_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_default_delimiter tests/test_core.py::test_delimited_list_with_datetime tests/test_core.py::test_delimited_list_custom_delimiter tests/test_core.py::test_delimited_list_load_list_errors tests/test_core.py::test_delimited_list_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_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_dict2schema tests/test_core.py::test_dict2schema_doesnt_add_to_class_registry tests/test_core.py::test_dict2schema_with_nesting 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
CHANGELOG.rst src/webargs/fields.py docs/upgrading.rst
CHANGELOG.rst src/webargs/fields.py docs/upgrading.rst