diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 703bfe6..138d5f1 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,20 +1,23 @@
 Changelog
 ---------
 
-7.1.0 (Unreleased)
+8.0.0 (Unreleased)
 ******************
 
 Features:
 
+* ``unknown`` defaults to `None` for body locations (`json`, `form` and
+  `json_or_form`) (:issue:`580`).
+
 * Detection of fields as "multi-value" for unpacking lists from multi-dict
-  types is now extensible with the `is_multiple` attribute. If a field sets
-  `is_multiple = True` it will be detected as a multi-value field.
+  types is now extensible with the ``is_multiple`` attribute. If a field sets
+  ``is_multiple = True`` it will be detected as a multi-value field.
   (:issue:`563`)
 
-* If `is_multiple` is not set or is set to `None`, webargs will check if the
-  field is an instance of `List`.
+* If ``is_multiple`` is not set or is set to ``None``, webargs will check if the
+  field is an instance of ``List`` or ``Tuple``.
 
-* A new attribute on `Parser` objects, ``Parser.KNOWN_MULTI_FIELDS`` can be
+* A new attribute on ``Parser`` objects, ``Parser.KNOWN_MULTI_FIELDS`` can be
   used to set fields which should be detected as ``is_multiple=True`` even when
   the attribute is not set.
 
diff --git a/setup.py b/setup.py
index 15c033b..b201de5 100644
--- a/setup.py
+++ b/setup.py
@@ -21,7 +21,7 @@ EXTRAS_REQUIRE = {
     + FRAMEWORKS,
     "lint": [
         "mypy==0.812",
-        "flake8==3.8.4",
+        "flake8==3.9.0",
         "flake8-bugbear==21.3.2",
         "pre-commit~=2.4",
     ],
diff --git a/src/webargs/aiohttpparser.py b/src/webargs/aiohttpparser.py
index 334e601..b85ff51 100644
--- a/src/webargs/aiohttpparser.py
+++ b/src/webargs/aiohttpparser.py
@@ -71,7 +71,7 @@ del _find_exceptions
 class AIOHTTPParser(AsyncParser):
     """aiohttp request argument parser."""
 
-    DEFAULT_UNKNOWN_BY_LOCATION = {
+    DEFAULT_UNKNOWN_BY_LOCATION: typing.Dict[str, typing.Optional[str]] = {
         "match_info": RAISE,
         "path": RAISE,
         **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION,
diff --git a/src/webargs/core.py b/src/webargs/core.py
index de8953d..3752479 100644
--- a/src/webargs/core.py
+++ b/src/webargs/core.py
@@ -126,10 +126,10 @@ class Parser:
     DEFAULT_LOCATION: str = "json"
     #: Default value to use for 'unknown' on schema load
     #  on a per-location basis
-    DEFAULT_UNKNOWN_BY_LOCATION: typing.Dict[str, str] = {
-        "json": ma.RAISE,
-        "form": ma.RAISE,
-        "json_or_form": ma.RAISE,
+    DEFAULT_UNKNOWN_BY_LOCATION: typing.Dict[str, typing.Optional[str]] = {
+        "json": None,
+        "form": None,
+        "json_or_form": None,
         "querystring": ma.EXCLUDE,
         "query": ma.EXCLUDE,
         "headers": ma.EXCLUDE,
@@ -142,9 +142,8 @@ class Parser:
     DEFAULT_VALIDATION_STATUS: int = DEFAULT_VALIDATION_STATUS
     #: Default error message for validation errors
     DEFAULT_VALIDATION_MESSAGE: str = "Invalid value."
-    # TODO: add ma.fields.Tuple in v8.0
     #: field types which should always be treated as if they set `is_multiple=True`
-    KNOWN_MULTI_FIELDS: typing.List[typing.Type] = [ma.fields.List]
+    KNOWN_MULTI_FIELDS: typing.List[typing.Type] = [ma.fields.List, ma.fields.Tuple]
 
     #: Maps location => method name
     __location_map__: typing.Dict[str, typing.Union[str, typing.Callable]] = {
diff --git a/src/webargs/flaskparser.py b/src/webargs/flaskparser.py
index 29bc93d..053988d 100644
--- a/src/webargs/flaskparser.py
+++ b/src/webargs/flaskparser.py
@@ -20,6 +20,8 @@ Example: ::
             uid=uid, per_page=args["per_page"]
         )
 """
+import typing
+
 import flask
 from werkzeug.exceptions import HTTPException
 
@@ -49,7 +51,7 @@ def is_json_request(req):
 class FlaskParser(core.Parser):
     """Flask request argument parser."""
 
-    DEFAULT_UNKNOWN_BY_LOCATION = {
+    DEFAULT_UNKNOWN_BY_LOCATION: typing.Dict[str, typing.Optional[str]] = {
         "view_args": ma.RAISE,
         "path": ma.RAISE,
         **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION,
diff --git a/src/webargs/multidictproxy.py b/src/webargs/multidictproxy.py
index 7017d96..a277178 100644
--- a/src/webargs/multidictproxy.py
+++ b/src/webargs/multidictproxy.py
@@ -18,7 +18,10 @@ class MultiDictProxy(Mapping):
         self,
         multidict,
         schema: ma.Schema,
-        known_multi_fields: typing.Tuple[typing.Type, ...] = (ma.fields.List,),
+        known_multi_fields: typing.Tuple[typing.Type, ...] = (
+            ma.fields.List,
+            ma.fields.Tuple,
+        ),
     ):
         self.data = multidict
         self.known_multi_fields = known_multi_fields
diff --git a/src/webargs/pyramidparser.py b/src/webargs/pyramidparser.py
index 46bb377..4be2884 100644
--- a/src/webargs/pyramidparser.py
+++ b/src/webargs/pyramidparser.py
@@ -25,6 +25,7 @@ Example usage: ::
         server.serve_forever()
 """
 import functools
+import typing
 from collections.abc import Mapping
 
 from webob.multidict import MultiDict
@@ -43,7 +44,7 @@ def is_json_request(req):
 class PyramidParser(core.Parser):
     """Pyramid request argument parser."""
 
-    DEFAULT_UNKNOWN_BY_LOCATION = {
+    DEFAULT_UNKNOWN_BY_LOCATION: typing.Dict[str, typing.Optional[str]] = {
         "matchdict": ma.RAISE,
         "path": ma.RAISE,
         **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION,

diff --git a/tests/test_core.py b/tests/test_core.py
index d7b35fb..1219659 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -35,7 +35,7 @@ class MockRequestParser(Parser):
     """A minimal parser implementation that parses mock requests."""
 
     def load_querystring(self, req, schema):
-        return MultiDictProxy(req.query, schema)
+        return self._makeproxy(req.query, schema)
 
     def load_json(self, req, schema):
         return req.json
@@ -1040,6 +1040,7 @@ def test_type_conversion_with_multiple_required(web_request, parser):
         "is_multiple_false",
         "is_multiple_notset",
         "list_field",
+        "tuple_field",
         "added_to_known",
     ],
 )
@@ -1103,13 +1104,20 @@ def test_is_multiple_detection(web_request, parser, input_dict, setting):
         args = {"foos": fields.List(fields.Str())}
         result = parser.parse(args, web_request, location="query")
         assert result["foos"] in (["a", "b"], ["b", "a"])
+    # case 5: the field is a Tuple (special case)
+    elif setting == "tuple_field":
+        # this should behave like the is_multiple=True case and produce a tuple
+        args = {"foos": fields.Tuple((fields.Str, fields.Str))}
+        result = parser.parse(args, web_request, location="query")
+        assert result["foos"] in (("a", "b"), ("b", "a"))
+    # case 6: the field is custom, but added to the known fields of the parser
     elif setting == "added_to_known":
         # if it's included in the known multifields and is_multiple is not set, behave
         # like is_multiple=True
         parser.KNOWN_MULTI_FIELDS.append(CustomMultiplexingField)
         args = {"foos": CustomMultiplexingField()}
         result = parser.parse(args, web_request, location="query")
-        assert result["foos"] in ("a", "b")
+        assert result["foos"] in (["a", "b"], ["b", "a"])
     else:
         raise NotImplementedError
 

Add marshmallow.fields.Tuple to detected `is_multiple` fields
I noticed this while working on #584 . Currently, `is_multiple(ma.fields.List(...)) == True`, but `is_multiple(ma.fields.Tuple(...)) == False`.
We should add `Tuple` so that `is_multiple` returns true.

It is possible that users have subclassed `fields.Tuple` or done something else which could make that a breaking change. I don't think that's very likely, but we are already queuing things up for 8.0 . I'm going to put this in the 8.0 milestone. We could probably do it sooner if there's a reason to do so.


I don't think using `Tuple` for a query param is a good idea. Supporting `...?foo=1&foo=2` but *not* `...?foo=1` or `...?foo=1&foo=2&foo=3` is unusual and therefore likely to be confusing. I'd advise webargs users not to do that. However! It's surprising that `Tuple` is not treated as `is_multiple=True`, and I'm just aiming for the least surprising behavior for webargs here.
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_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[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 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_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_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_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[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