https://github.com/python/cpython/commit/a3435d5ccc1878db52f48b0a5944cd0a78ec8fcf
commit: a3435d5ccc1878db52f48b0a5944cd0a78ec8fcf
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-05-03T10:24:57Z
summary:

gh-143231: Do not swallow not matched warnings in assertWarns*() (GH-149229)

unittest.TestCase methods assertWarns() and assertWarnsRegex() no longer
swallow warnings that do not match the specified category or regex.
Nested context managers are now supported.

files:
A Misc/NEWS.d/next/Library/2026-05-01-11-39-37.gh-issue-143231.0cOHET.rst
M Doc/library/unittest.rst
M Doc/whatsnew/3.15.rst
M Lib/test/test_complex.py
M Lib/test/test_unittest/test_assertions.py
M Lib/test/test_unittest/test_case.py
M Lib/unittest/case.py

diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst
index 6e0df0648fb8bf..d55bc9f9662360 100644
--- a/Doc/library/unittest.rst
+++ b/Doc/library/unittest.rst
@@ -1095,6 +1095,13 @@ Test cases
          self.assertIn('myfile.py', cm.filename)
          self.assertEqual(320, cm.lineno)
 
+      The context managers can be nested to test that multiple different
+      warnings are emitted::
+
+         with (self.assertWarns(SomeWarning),
+               self.assertWarns(OtherWarning)):
+             do_something()
+
       This method works regardless of the warning filters in place when it
       is called.
 
@@ -1103,6 +1110,10 @@ Test cases
       .. versionchanged:: 3.3
          Added the *msg* keyword argument when used as a context manager.
 
+      .. versionchanged:: next
+         Warnings that do not match the specified category are no longer
+         swallowed.
+         Nested context managers are now supported.
 
    .. method:: assertWarnsRegex(warning, regex, callable, *args, **kwds)
                assertWarnsRegex(warning, regex, *, msg=None)
@@ -1121,11 +1132,23 @@ Test cases
          with self.assertWarnsRegex(RuntimeWarning, 'unsafe frobnicating'):
              frobnicate('/etc/passwd')
 
+      The context managers can be nested to test that multiple different
+      warnings are emitted::
+
+         with (self.assertWarns(SomeWarning, regex1),
+               self.assertWarns(OtherWarning, regex2)):
+             do_something()
+
       .. versionadded:: 3.2
 
       .. versionchanged:: 3.3
          Added the *msg* keyword argument when used as a context manager.
 
+      .. versionchanged:: next
+         Warnings that do not match the specified category or regex are
+         no longer swallowed.
+         Nested context managers are now supported.
+
    .. method:: assertLogs(logger=None, level=None, formatter=None)
 
       A context manager to test that at least one message is logged on
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index 78e464f2a5a6d8..bf7aecc4c35bd1 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -1471,10 +1471,16 @@ unicodedata
 unittest
 --------
 
-* :func:`unittest.TestCase.assertLogs` will now accept a formatter
+* :meth:`unittest.TestCase.assertLogs` will now accept a formatter
   to control how messages are formatted.
   (Contributed by Garry Cairns in :gh:`134567`.)
 
+* :meth:`unittest.TestCase.assertWarns` and
+  :meth:`unittest.TestCase.assertWarnsRegex` no longer swallow warnings that
+  do not match the specified category or regex.
+  Nested context managers are now supported.
+  (Contributed by Serhiy Storchaka in :gh:`143231`.)
+
 
 urllib.parse
 ------------
@@ -2352,3 +2358,11 @@ that may require changes to your code.
   with argument ``altchars=b'-_'`` (this works with older Python versions)
   to make padding required.
   (Contributed by Serhiy Storchaka in :gh:`73613`.)
+
+* Since :meth:`unittest.TestCase.assertWarns` and
+  :meth:`unittest.TestCase.assertWarnsRegex` no longer swallow warnings that
+  do not match the specified category or regex, your tests may start leaking
+  some warnings that were previously masked.
+  Use warning filters to silence them or additional :meth:`!assertWarns*`
+  to catch and check them.
+  (Contributed by Serhiy Storchaka in :gh:`143231`.)
diff --git a/Lib/test/test_complex.py b/Lib/test/test_complex.py
index bee2aceb187027..bb307191dffcc1 100644
--- a/Lib/test/test_complex.py
+++ b/Lib/test/test_complex.py
@@ -504,17 +504,25 @@ def check(z, x, y):
         with self.assertWarnsRegex(DeprecationWarning,
                 "argument 'imag' must be a real number, not complex"):
             check(complex(0.0, 4.25j), -4.25, 0.0)
-        with self.assertWarnsRegex(DeprecationWarning,
-                "argument 'real' must be a real number, not complex"):
+        with (self.assertWarnsRegex(DeprecationWarning,
+                "argument 'real' must be a real number, not complex"),
+              self.assertWarnsRegex(DeprecationWarning,
+                "argument 'imag' must be a real number, not complex")):
             check(complex(4.25+0j, 0j), 4.25, 0.0)
-        with self.assertWarnsRegex(DeprecationWarning,
-                "argument 'real' must be a real number, not complex"):
+        with (self.assertWarnsRegex(DeprecationWarning,
+                "argument 'real' must be a real number, not complex"),
+              self.assertWarnsRegex(DeprecationWarning,
+                "argument 'imag' must be a real number, not complex")):
             check(complex(4.25j, 0j), 0.0, 4.25)
-        with self.assertWarnsRegex(DeprecationWarning,
-                "argument 'real' must be a real number, not complex"):
+        with (self.assertWarnsRegex(DeprecationWarning,
+                "argument 'real' must be a real number, not complex"),
+              self.assertWarnsRegex(DeprecationWarning,
+                "argument 'imag' must be a real number, not complex")):
             check(complex(0j, 4.25+0j), 0.0, 4.25)
-        with self.assertWarnsRegex(DeprecationWarning,
-                "argument 'real' must be a real number, not complex"):
+        with (self.assertWarnsRegex(DeprecationWarning,
+                "argument 'real' must be a real number, not complex"),
+              self.assertWarnsRegex(DeprecationWarning,
+                "argument 'imag' must be a real number, not complex")):
             check(complex(0j, 4.25j), -4.25, 0.0)
 
         check(complex(real=4.25), 4.25, 0.0)
diff --git a/Lib/test/test_unittest/test_assertions.py 
b/Lib/test/test_unittest/test_assertions.py
index 1dec947ea76d23..df95e558949aee 100644
--- a/Lib/test/test_unittest/test_assertions.py
+++ b/Lib/test/test_unittest/test_assertions.py
@@ -406,11 +406,12 @@ def testAssertWarnsRegex(self):
         # test warning raised but with wrong message
         def raise_wrong_message():
             warnings.warn('foo')
-        self.assertMessagesCM('assertWarnsRegex', (UserWarning, 'regex'),
-                              raise_wrong_message,
-                              ['^"regex" does not match "foo"$', '^oops$',
-                               '^"regex" does not match "foo"$',
-                               '^"regex" does not match "foo" : oops$'])
+        with self.assertWarnsRegex(UserWarning, 'foo'):
+            self.assertMessagesCM('assertWarnsRegex', (UserWarning, 'regex'),
+                                  raise_wrong_message,
+                                  ['^"regex" does not match "foo"$', '^oops$',
+                                   '^"regex" does not match "foo"$',
+                                   '^"regex" does not match "foo" : oops$'])
 
 
 if __name__ == "__main__":
diff --git a/Lib/test/test_unittest/test_case.py 
b/Lib/test/test_unittest/test_case.py
index cf10e956bf2bdc..83b42918e1eff6 100644
--- a/Lib/test/test_unittest/test_case.py
+++ b/Lib/test/test_unittest/test_case.py
@@ -1631,11 +1631,11 @@ def testAssertRaisesRegexNoExceptionType(self):
             self.assertRaisesRegex((ValueError, object), 'expect')
 
     def testAssertWarnsCallable(self):
-        def _runtime_warn():
-            warnings.warn("foo", RuntimeWarning)
+        def _runtime_warn(categories=(RuntimeWarning,)):
+            for category in categories:
+                warnings.warn("foo", category)
         # Success when the right warning is triggered, even several times
-        self.assertWarns(RuntimeWarning, _runtime_warn)
-        self.assertWarns(RuntimeWarning, _runtime_warn)
+        self.assertWarns(RuntimeWarning, _runtime_warn, (RuntimeWarning, 
RuntimeWarning))
         # A tuple of warning classes is accepted
         self.assertWarns((DeprecationWarning, RuntimeWarning), _runtime_warn)
         # *args and **kwargs also work
@@ -1648,22 +1648,35 @@ def _runtime_warn():
         with self.assertRaises(TypeError):
             self.assertWarns(RuntimeWarning, None)
         # Failure when another warning is triggered
-        with warnings.catch_warnings():
+        with warnings.catch_warnings(record=True) as log:
             # Force default filter (in case tests are run with -We)
             warnings.simplefilter("default", RuntimeWarning)
             with self.assertRaises(self.failureException):
-                self.assertWarns(DeprecationWarning, _runtime_warn)
+                self.assertWarns(DeprecationWarning, _runtime_warn,
+                                 (RuntimeWarning, RuntimeWarning))
+        self.assertEqual(len(log), 1, log)
+        self.assertIsInstance(log[0].message, RuntimeWarning)
         # Filters for other warnings are not modified
         with warnings.catch_warnings():
             warnings.simplefilter("error", RuntimeWarning)
             with self.assertRaises(RuntimeWarning):
                 self.assertWarns(DeprecationWarning, _runtime_warn)
+        # Warnings that do not match the category are not swallowed.
+        with self.assertWarns(RuntimeWarning):
+            with self.assertRaises(self.failureException):
+                self.assertWarns(DeprecationWarning, _runtime_warn)
+        with self.assertWarns(RuntimeWarning):
+            self.assertWarns(DeprecationWarning, _runtime_warn,
+                             (RuntimeWarning, DeprecationWarning))
+        with self.assertWarns(RuntimeWarning):
+            self.assertWarns(DeprecationWarning, _runtime_warn,
+                             (DeprecationWarning, RuntimeWarning))
 
     def testAssertWarnsContext(self):
         # Believe it or not, it is preferable to duplicate all tests above,
         # to make sure the __warningregistry__ $@ is circumvented correctly.
-        def _runtime_warn():
-            warnings.warn("foo", RuntimeWarning)
+        def _runtime_warn(category=RuntimeWarning):
+            warnings.warn("foo", category)
         _runtime_warn_lineno = inspect.getsourcelines(_runtime_warn)[1]
         with self.assertWarns(RuntimeWarning) as cm:
             _runtime_warn()
@@ -1694,18 +1707,58 @@ def _runtime_warn():
             with self.assertWarns(RuntimeWarning, foobar=42):
                 pass
         # Failure when another warning is triggered
-        with warnings.catch_warnings():
+        with warnings.catch_warnings(record=True) as log:
             # Force default filter (in case tests are run with -We)
             warnings.simplefilter("default", RuntimeWarning)
             with self.assertRaises(self.failureException):
                 with self.assertWarns(DeprecationWarning):
                     _runtime_warn()
+                    _runtime_warn()
+        self.assertEqual(len(log), 1, log)
+        self.assertIsInstance(log[0].message, RuntimeWarning)
+        with warnings.catch_warnings(record=True) as log:
+            # Force default filter (in case tests are run with -We)
+            warnings.simplefilter("error", RuntimeWarning)
+            warnings.filterwarnings("default", category=RuntimeWarning,
+                                    module=__name__)
+            with self.assertRaises(self.failureException):
+                with self.assertWarns(DeprecationWarning):
+                    _runtime_warn()
+                    _runtime_warn()
+        self.assertEqual(len(log), 1, log)
+        self.assertIsInstance(log[0].message, RuntimeWarning)
         # Filters for other warnings are not modified
         with warnings.catch_warnings():
             warnings.simplefilter("error", RuntimeWarning)
             with self.assertRaises(RuntimeWarning):
                 with self.assertWarns(DeprecationWarning):
                     _runtime_warn()
+        # Warnings that do not match the category are not swallowed.
+        with self.assertWarns(RuntimeWarning):
+            with self.assertRaises(self.failureException):
+                with self.assertWarns(DeprecationWarning):
+                    _runtime_warn()
+        with self.assertWarns(RuntimeWarning):
+            with self.assertWarns(DeprecationWarning):
+                _runtime_warn()
+                _runtime_warn(DeprecationWarning)
+        with self.assertWarns(RuntimeWarning):
+            with self.assertWarns(DeprecationWarning):
+                _runtime_warn(DeprecationWarning)
+                _runtime_warn()
+        # Filters by module name work for other warnings.
+        with warnings.catch_warnings(record=True) as log:
+            warnings.filterwarnings("error", category=RuntimeWarning)
+            warnings.filterwarnings("default", category=RuntimeWarning,
+                                    module=re.escape(__name__))
+            warnings.filterwarnings("error", category=RuntimeWarning,
+                                    module='test_case')
+            with self.assertWarns(DeprecationWarning):
+                _runtime_warn(DeprecationWarning)
+                _runtime_warn()
+                _runtime_warn()
+        self.assertEqual(len(log), 1, log)
+        self.assertIsInstance(log[0].message, RuntimeWarning)
 
     def testAssertWarnsNoExceptionType(self):
         with self.assertRaises(TypeError):
@@ -1722,8 +1775,9 @@ def testAssertWarnsNoExceptionType(self):
             self.assertWarns((UserWarning, Exception))
 
     def testAssertWarnsRegexCallable(self):
-        def _runtime_warn(msg):
-            warnings.warn(msg, RuntimeWarning)
+        def _runtime_warn(*msgs):
+            for msg in msgs:
+                warnings.warn(msg, RuntimeWarning)
         self.assertWarnsRegex(RuntimeWarning, "o+",
                               _runtime_warn, "foox")
         # Failure when no warning is triggered
@@ -1734,16 +1788,26 @@ def _runtime_warn(msg):
         with self.assertRaises(TypeError):
             self.assertWarnsRegex(RuntimeWarning, "o+", None)
         # Failure when another warning is triggered
-        with warnings.catch_warnings():
+        with warnings.catch_warnings(record=True) as log:
             # Force default filter (in case tests are run with -We)
             warnings.simplefilter("default", RuntimeWarning)
             with self.assertRaises(self.failureException):
                 self.assertWarnsRegex(DeprecationWarning, "o+",
-                                      _runtime_warn, "foox")
-        # Failure when message doesn't match
-        with self.assertRaises(self.failureException):
+                                      _runtime_warn, "foox", "foox")
+        self.assertEqual(len(log), 1, log)
+        self.assertIsInstance(log[0].message, RuntimeWarning)
+        # Failure when message doesn't match.
+        # Warnings that do not match the regex are not swallowed.
+        with self.assertWarnsRegex(RuntimeWarning, "ar"):
+            with self.assertRaises(self.failureException):
+                self.assertWarnsRegex(RuntimeWarning, "o+",
+                                      _runtime_warn, "barz")
+        with self.assertWarnsRegex(RuntimeWarning, "ar"):
             self.assertWarnsRegex(RuntimeWarning, "o+",
-                                  _runtime_warn, "barz")
+                                  _runtime_warn, "barz", "foox")
+        with self.assertWarnsRegex(RuntimeWarning, "ar"):
+            self.assertWarnsRegex(RuntimeWarning, "o+",
+                                  _runtime_warn, "foox", "barz")
         # A little trickier: we ask RuntimeWarnings to be raised, and then
         # check for some of them.  It is implementation-defined whether
         # non-matching RuntimeWarnings are simply re-raised, or produce a
@@ -1778,15 +1842,28 @@ def _runtime_warn(msg):
             with self.assertWarnsRegex(RuntimeWarning, 'o+', foobar=42):
                 pass
         # Failure when another warning is triggered
-        with warnings.catch_warnings():
+        with warnings.catch_warnings(record=True) as log:
             # Force default filter (in case tests are run with -We)
             warnings.simplefilter("default", RuntimeWarning)
             with self.assertRaises(self.failureException):
                 with self.assertWarnsRegex(DeprecationWarning, "o+"):
                     _runtime_warn("foox")
-        # Failure when message doesn't match
-        with self.assertRaises(self.failureException):
+                    _runtime_warn("foox")
+        self.assertEqual(len(log), 1, log)
+        self.assertIsInstance(log[0].message, RuntimeWarning)
+        # Failure when message doesn't match.
+        # Warnings that do not match the regex are not swallowed.
+        with self.assertWarnsRegex(RuntimeWarning, "ar"):
+            with self.assertRaises(self.failureException):
+                with self.assertWarnsRegex(RuntimeWarning, "o+"):
+                    _runtime_warn("barz")
+        with self.assertWarnsRegex(RuntimeWarning, "ar"):
+            with self.assertWarnsRegex(RuntimeWarning, "o+"):
+                _runtime_warn("barz")
+                _runtime_warn("foox")
+        with self.assertWarnsRegex(RuntimeWarning, "ar"):
             with self.assertWarnsRegex(RuntimeWarning, "o+"):
+                _runtime_warn("foox")
                 _runtime_warn("barz")
         # A little trickier: we ask RuntimeWarnings to be raised, and then
         # check for some of them.  It is implementation-defined whether
@@ -1797,6 +1874,19 @@ def _runtime_warn(msg):
             with self.assertRaises((RuntimeWarning, self.failureException)):
                 with self.assertWarnsRegex(RuntimeWarning, "o+"):
                     _runtime_warn("barz")
+        # Filters by module name work for warnings with other message.
+        with warnings.catch_warnings(record=True) as log:
+            warnings.filterwarnings("error", category=RuntimeWarning)
+            warnings.filterwarnings("default", category=RuntimeWarning,
+                                    module=re.escape(__name__))
+            warnings.filterwarnings("error", category=RuntimeWarning,
+                                    module='test_case')
+            with self.assertWarnsRegex(RuntimeWarning, "ar"):
+                _runtime_warn("bar")
+                _runtime_warn("foox")
+                _runtime_warn("foox")
+        self.assertEqual(len(log), 1, log)
+        self.assertIsInstance(log[0].message, RuntimeWarning)
 
     def testAssertWarnsRegexNoExceptionType(self):
         with self.assertRaises(TypeError):
diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py
index eba50839cd33ae..a392238c85abfa 100644
--- a/Lib/unittest/case.py
+++ b/Lib/unittest/case.py
@@ -301,7 +301,7 @@ def __enter__(self):
                 v.__warningregistry__ = {}
         self.warnings_manager = warnings.catch_warnings(record=True)
         self.warnings = self.warnings_manager.__enter__()
-        warnings.simplefilter("always", self.expected)
+        warnings.simplefilter("always")
         return self
 
     def __exit__(self, exc_type, exc_value, tb):
@@ -314,19 +314,44 @@ def __exit__(self, exc_type, exc_value, tb):
         except AttributeError:
             exc_name = str(self.expected)
         first_matching = None
+        matched = False
+        non_matching_warnings = []
         for m in self.warnings:
             w = m.message
             if not isinstance(w, self.expected):
+                non_matching_warnings.append(m)
                 continue
             if first_matching is None:
                 first_matching = w
             if (self.expected_regex is not None and
                 not self.expected_regex.search(str(w))):
+                non_matching_warnings.append(m)
                 continue
+            if matched:
+                continue
+            matched = True
             # store warning for later retrieval
             self.warning = w
             self.filename = m.filename
             self.lineno = m.lineno
+        for m in non_matching_warnings:
+            module = m.module
+            module_globals = None
+            registry = None
+            if module is not None:
+                try:
+                    module_globals = vars(sys.modules[module])
+                except (KeyError, TypeError):
+                    # module == "<string>" or sys.modules[module] is None
+                    pass
+                else:
+                    registry = 
module_globals.setdefault("__warningregistry__", {})
+            warnings.warn_explicit(m.message, m.category, m.filename, m.lineno,
+                                   module=module,
+                                   registry=registry,
+                                   module_globals=module_globals,
+                                   source=m.source)
+        if matched:
             return
         # Now we simply try to choose a helpful failure message
         if first_matching is not None:
@@ -338,7 +363,6 @@ def __exit__(self, exc_type, exc_value, tb):
         else:
             self._raiseFailure("{} not triggered".format(exc_name))
 
-
 class _AssertNotWarnsContext(_AssertWarnsContext):
 
     def __exit__(self, exc_type, exc_value, tb):
diff --git 
a/Misc/NEWS.d/next/Library/2026-05-01-11-39-37.gh-issue-143231.0cOHET.rst 
b/Misc/NEWS.d/next/Library/2026-05-01-11-39-37.gh-issue-143231.0cOHET.rst
new file mode 100644
index 00000000000000..05c9fa79904154
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-05-01-11-39-37.gh-issue-143231.0cOHET.rst
@@ -0,0 +1,4 @@
+:func:`unittest.TestCase.assertWarns` and
+:func:`unittest.TestCase.assertWarnsRegex` no longer swallow warnings that
+do not match the specified category or regex.
+Nested context managers are now supported.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to