https://github.com/python/cpython/commit/2cf2dd60df4df9a02da9048e7737ad1eb5f7b93b
commit: 2cf2dd60df4df9a02da9048e7737ad1eb5f7b93b
branch: main
author: Vasiliy Kiryanov <[email protected]>
committer: orsenthil <[email protected]>
date: 2026-10-31T17:01:13Z
summary:

gh-50720: Set errno and strerror on URLError wrapping an OSError (#160938)

URLError will set errno and strerror attributes from the OSError 

Co-authored-by: Senthil Kumaran <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-10-29-20-52-57.gh-issue-50720.Kd1VMR.rst
M Doc/library/urllib.error.rst
M Lib/test/test_urllib2.py
M Lib/urllib/error.py

diff --git a/Doc/library/urllib.error.rst b/Doc/library/urllib.error.rst
index b8864e36981ab9b..6a71d4a2ebb6845 100644
--- a/Doc/library/urllib.error.rst
+++ b/Doc/library/urllib.error.rst
@@ -27,6 +27,10 @@ The following exceptions are raised by :mod:`!urllib.error` 
as appropriate:
       :exc:`URLError` used to be a subtype of :exc:`IOError`, which is now an
       alias of :exc:`OSError`.
 
+   .. versionchanged:: 3.16
+      The :attr:`~OSError.errno` and :attr:`~OSError.strerror` attributes are
+      now set from :attr:`reason` when it is an :exc:`OSError`.
+
 
 .. exception:: HTTPError(url, code, msg, hdrs, fp)
 
diff --git a/Lib/test/test_urllib2.py b/Lib/test/test_urllib2.py
index a73b6ea96c34e51..5bfe5f4f4f03087 100644
--- a/Lib/test/test_urllib2.py
+++ b/Lib/test/test_urllib2.py
@@ -82,6 +82,27 @@ def test_parse_http_list(self):
     def test_URLError_reasonstr(self):
         err = urllib.error.URLError('reason')
         self.assertIn(err.reason, str(err))
+        self.assertIsNone(err.errno)
+        self.assertIsNone(err.strerror)
+
+    def test_URLError_OSError_attributes(self):
+        # gh-50720: errno and strerror are copied from a wrapped OSError.
+        socket_error = socket.gaierror(-2, 'Name or service not known')
+
+        class FailingHTTPConnection(MockHTTPClass):
+            def request(self, *args, **kwargs):
+                raise socket_error
+
+        h = urllib.request.AbstractHTTPHandler()
+        req = Request("http://example.com/";)
+        req.timeout = None
+
+        with self.assertRaises(urllib.error.URLError) as cm:
+            h.do_open(FailingHTTPConnection(), req)
+        err = cm.exception
+        self.assertIs(err.reason, socket_error)
+        self.assertEqual(err.errno, -2)
+        self.assertEqual(err.strerror, 'Name or service not known')
 
 
 class RequestHdrsTests(unittest.TestCase):
diff --git a/Lib/urllib/error.py b/Lib/urllib/error.py
index beb32d0df48610d..5020a60f1ef9bd0 100644
--- a/Lib/urllib/error.py
+++ b/Lib/urllib/error.py
@@ -27,6 +27,11 @@ def __init__(self, reason, filename=None):
         self.reason = reason
         if filename is not None:
             self.filename = filename
+        if isinstance(reason, OSError):
+            # OSError.__init__() is not called, so copy the attributes
+            # it would have set from the wrapped error (gh-50720).
+            self.errno = reason.errno
+            self.strerror = reason.strerror
 
     def __str__(self):
         return '<urlopen error %s>' % self.reason
diff --git 
a/Misc/NEWS.d/next/Library/2026-10-29-20-52-57.gh-issue-50720.Kd1VMR.rst 
b/Misc/NEWS.d/next/Library/2026-10-29-20-52-57.gh-issue-50720.Kd1VMR.rst
new file mode 100644
index 000000000000000..ddb4377c3c25987
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-10-29-20-52-57.gh-issue-50720.Kd1VMR.rst
@@ -0,0 +1,3 @@
+:exc:`urllib.error.URLError` now sets the :attr:`~OSError.errno` and
+:attr:`~OSError.strerror` attributes from
+:attr:`~urllib.error.URLError.reason` when it is an :exc:`OSError`.

_______________________________________________
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