coveralls: 
[![Coverage Status](https://coveralls.io/builds/2031381/badge)](https://coveralls.io/builds/2031381)

Coverage remained the same at 100.0% when pulling **8f5f8492be58f84c36c8751ec81a90b9d4f64353 on collect-all-errors** into **a60e8de415d9ed855570fc09ee14a5974532cf07 on master**.

diff --git a/README.rst b/README.rst
index bc413c0..33d435d 100644
--- a/README.rst
+++ b/README.rst
@@ -184,11 +184,10 @@ not missing any of the keys specified (unless they are specified as
     >>> schema.validates({'foo': 12, 'bar': 888, 'baz': 299})
     True
 
-    >>> schema.validate({'foo': 'bar'})
+    >>> schema.validate({'foo': 'bar', 'baz': 'qux'})
     Traceback (most recent call last):
         ...
-    val.NotValid: 'foo': 'bar' is not of type <class 'int'>
-
+    val.NotValid: ("'foo': 'bar' is not of type <class 'int'>", "'baz': 'qux' not matched")
     >>> schema.validate({'qux': 19})
     Traceback (most recent call last):
         ...
@@ -286,7 +285,7 @@ elements passed into the Or:
     >>> schema.validate('bar')
     Traceback (most recent call last):
         ...
-    val.NotValid: 'bar' is not equal to 'foo', 'bar' is not of type <class 'int'>
+    val.NotValid: 'bar' is not equal to 'foo' and 'bar' is not of type <class 'int'>
 
 
 And()
diff --git a/pp.yaml b/pp.yaml
index f3a43d0..bdcd9e8 100644
--- a/pp.yaml
+++ b/pp.yaml
@@ -10,8 +10,6 @@ ignore-patterns:
 
 pep8:
   run: true
-  options:
-    max-line-length: 80
 
 mccabe:
   run: true
@@ -44,3 +42,5 @@ pylint:
   disable:
     - too-few-public-methods
     - invalid-name
+    - star-args
+    - line-too-long
diff --git a/requirements.txt b/requirements.txt
index b8313ef..20b74fc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,1 @@
-teleport
 pyRFC3339>=0.2
diff --git a/val/__init__.py b/val/__init__.py
index b22cb10..42fd75a 100644
--- a/val/__init__.py
+++ b/val/__init__.py
@@ -60,7 +60,7 @@ def build_callable_validator(function):
                 return data
 
         except (TypeError, ValueError, NotValid) as ex:
-            raise NotValid(', '.join(ex.args))
+            raise NotValid(ex.args)
 
         raise NotValid("%r invalidated by '%s'" % (data, get_repr(function)))
 
@@ -116,14 +116,17 @@ def _determine_keys(dictionary):
 
 def _validate_mandatory_keys(mandatory, validated, data, to_validate):
     """Validate the manditory keys."""
+    errors = []
     for key, sub_schema in mandatory.items():
         if key not in data:
-            raise NotValid('missing key: %r' % (key,))
+            errors.append('missing key: %r' % (key,))
+            continue
         try:
             validated[key] = sub_schema(data[key])
         except NotValid as ex:
-            raise NotValid('%r: %s' % (key, ', '.join(ex.args)))
+            errors.extend(['%r: %s' % (key, arg) for arg in ex.args])
         to_validate.remove(key)
+    return errors
 
 
 def _validate_optional_key(key, missing, value, validated, optional):
@@ -131,9 +134,10 @@ def _validate_optional_key(key, missing, value, validated, optional):
     try:
         validated[key] = optional[key](value)
     except NotValid as ex:
-        raise NotValid('%r: %s' % (key, ', '.join(ex.args)))
+        return ['%r: %s' % (key, arg) for arg in ex.args]
     if key in missing:
         missing.remove(key)
+    return []
 
 
 def _validate_type_key(key, value, types, validated):
@@ -146,20 +150,24 @@ def _validate_type_key(key, value, types, validated):
         except NotValid:
             continue
         else:
-            break
-    else:
-        raise NotValid('%r: %r not matched' % (key, value))
+            return []
+
+    return ['%r: %r not matched' % (key, value)]
 
 
 def _validate_other_keys(optional, types, missing, validated, data,
                          to_validate):
     """Validate the rest of the keys present in the data."""
+    errors = []
     for key in to_validate:
         value = data[key]
         if key in optional:
-            _validate_optional_key(key, missing, value, validated, optional)
+            errors.extend(
+                _validate_optional_key(
+                    key, missing, value, validated, optional))
             continue
-        _validate_type_key(key, value, types, validated)
+        errors.extend(_validate_type_key(key, value, types, validated))
+    return errors
 
 
 def build_dict_validator(dictionary):
@@ -174,9 +182,13 @@ def build_dict_validator(dictionary):
 
         validated = {}
         to_validate = list(data.keys())
-        _validate_mandatory_keys(mandatory, validated, data, to_validate)
-        _validate_other_keys(
-            optional, types, missing, validated, data, to_validate)
+        errors = _validate_mandatory_keys(
+            mandatory, validated, data, to_validate)
+        errors.extend(
+            _validate_other_keys(
+                optional, types, missing, validated, data, to_validate))
+        if errors:
+            raise NotValid(*errors)
         for key in missing:
             validated[key] = defaults[key][0]
 
@@ -233,11 +245,14 @@ class BaseSchema(object):
     def validate(self, data):
         """Validate data. Raise NotValid error for invalid data."""
         validated = self._validated(data)
+        errors = []
         for validator in self.additional_validators:
             if not validator(validated):
-                raise NotValid(
+                errors.append(
                     "%s invalidated by '%s'" % (
                         validated, get_repr(validator)))
+        if errors:
+            raise NotValid(*errors)
 
         if self.default is UNSPECIFIED:
             return validated
@@ -303,7 +318,7 @@ class Or(BaseSchema):
             except NotValid as ex:
                 errors.extend(ex.args)
 
-        raise NotValid(', '.join(errors))
+        raise NotValid(' and '.join(errors))
 
     def __repr__(self):
         return "<%s>" % (" or ".join(["%r" % (v,) for v in self.values]),)
@@ -347,7 +362,7 @@ class Convert(BaseSchema):
         try:
             return self.convert(data)
         except (TypeError, ValueError) as ex:
-            raise NotValid(', '.join(ex.args))
+            raise NotValid(*ex.args)
 
     def __repr__(self):
         """Display schema."""

Return all validation errors, rather than just the first one.
Currently val's ValidationErrors only contain a single validation error message, even if the data was invalid in more than one way. This makes validation a tiny bit faster, but at the expense of not being as informative as it could be. I think I am going to reverse this decision.
diff --git a/tests/test_val.py b/tests/test_val.py
index c60bf63..fa4825b 100644
--- a/tests/test_val.py
+++ b/tests/test_val.py
@@ -487,3 +487,10 @@ def test_cannot_change_definition():
     schema = Schema({"foo": "bar"})
     with pytest.raises(AttributeError):
         schema.definition = {"qux": "baz"}
+
+
+def test_captures_multiple_errors():
+    schema = Schema({"foo": str})
+    with pytest.raises(NotValid) as exception:
+        schema.validate({'foo': 12, 'bar': 'qux'})
+    assert len(exception.value.args) == 2

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest pytest-cov flake8 coveralls schema
3.4
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==22.2.0
certifi==2021.5.30
charset-normalizer==2.0.12
coverage==6.2
coveralls==3.3.1
docopt==0.6.2
flake8==5.0.4
idna==3.10
importlib-metadata==4.2.0
iniconfig==1.1.1
mccabe==0.7.0
packaging==21.3
pluggy==1.0.0
py==1.11.0
pycodestyle==2.9.1
pyflakes==2.5.0
pyparsing==3.1.4
pyRFC3339==2.0.1
pytest==7.0.1
pytest-cov==4.0.0
requests==2.27.1
schema==0.7.7
teleport==0.4.0
tomli==1.2.3
typing_extensions==4.1.1
urllib3==1.26.20
-e git+https://github.com/thisfred/val.git@a60e8de415d9ed855570fc09ee14a5974532cf07#egg=val
zipp==3.6.0

name: val
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
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - 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=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==22.2.0
      - charset-normalizer==2.0.12
      - coverage==6.2
      - coveralls==3.3.1
      - docopt==0.6.2
      - flake8==5.0.4
      - idna==3.10
      - importlib-metadata==4.2.0
      - iniconfig==1.1.1
      - mccabe==0.7.0
      - packaging==21.3
      - pluggy==1.0.0
      - py==1.11.0
      - pycodestyle==2.9.1
      - pyflakes==2.5.0
      - pyparsing==3.1.4
      - pyrfc3339==2.0.1
      - pytest==7.0.1
      - pytest-cov==4.0.0
      - requests==2.27.1
      - schema==0.7.7
      - teleport==0.4.0
      - tomli==1.2.3
      - typing-extensions==4.1.1
      - urllib3==1.26.20
      - zipp==3.6.0
prefix: /opt/conda/envs/val

tests/test_val.py::test_captures_multiple_errors
tests/test_val.py::test_must_implement_validated tests/test_val.py::test_identity tests/test_val.py::test_non_identity tests/test_val.py::test_type_check tests/test_val.py::test_failing_type_check tests/test_val.py::test_dictionary tests/test_val.py::test_dictionary_not_a_dict tests/test_val.py::test_dictionary_optional tests/test_val.py::test_dictionary_optional_repr tests/test_val.py::test_dictionary_optional_with_default_on_value tests/test_val.py::test_dictionary_optional_with_null_values tests/test_val.py::test_dictionary_missing_with_null_values tests/test_val.py::test_regression_validating_twice_works tests/test_val.py::test_dictionary_optional_with_null_value_on_value_schema tests/test_val.py::test_dictionary_optional_not_missing tests/test_val.py::test_dictionary_optional_with_default_on_value_not_missing tests/test_val.py::test_dictionary_wrong_key tests/test_val.py::test_dictionary_missing_key tests/test_val.py::test_dictionary_leftover_key tests/test_val.py::test_list_data tests/test_val.py::test_list_data_wrong_type tests/test_val.py::test_list_data_multiple_types tests/test_val.py::test_not_list tests/test_val.py::test_list_not_found tests/test_val.py::test_or tests/test_val.py::test_or_repr tests/test_val.py::test_nullable tests/test_val.py::test_nullable_with_default tests/test_val.py::test_and tests/test_val.py::test_and_repr tests/test_val.py::test_dont_care_values_in_dict tests/test_val.py::test_callable tests/test_val.py::test_callable_gives_readable_error tests/test_val.py::test_callable_gives_sensible_error tests/test_val.py::test_convert tests/test_val.py::test_ordered tests/test_val.py::test_ordered_repr tests/test_val.py::test_callable_exception tests/test_val.py::test_subschemas tests/test_val.py::test_and_schema tests/test_val.py::test_or_schema tests/test_val.py::test_validate_list tests/test_val.py::test_list_tuple_set_frozenset tests/test_val.py::test_strictly tests/test_val.py::test_dict tests/test_val.py::test_dict_keys tests/test_val.py::test_dict_optional_keys tests/test_val.py::test_validate_object tests/test_val.py::test_issue_9_prioritized_key_comparison tests/test_val.py::test_issue_9_prioritized_key_comparison_in_dicts tests/test_val.py::test_schema_with_additional_validators tests/test_val.py::test_does_not_use_default_value_to_replace_falsy_values tests/test_val.py::test_uses_default_value_when_explicitly_told_to tests/test_val.py::test_uses_default_value_to_replace_missing_values tests/test_val.py::test_can_see_definition tests/test_val.py::test_cannot_change_definition
README.rst val/__init__.py requirements.txt pp.yaml
README.rst val/__init__.py requirements.txt pp.yaml