Skip to content

Commit 71331e7

Browse files
gh-109714: Improve the OSError constructor
* The errno argument can now be omitted. It defaults to the error code which corresponds to the exception class. * The strerror argument can now be omitted. It is derived from the resulting errno, or on Windows from winerror. * filename, winerror and filename2 can now be passed by keyword, as can characters_written for BlockingIOError. * args is no longer truncated when a file name is given, so that an exception carrying one survives pickling. * Passing more than five positional arguments is now a TypeError. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent ee1da7e commit 71331e7

11 files changed

Lines changed: 616 additions & 292 deletions

File tree

Doc/library/exceptions.rst

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -357,20 +357,32 @@ The following exceptions are the exceptions that are usually raised.
357357
the built-in constant.
358358

359359

360-
.. exception:: OSError([arg])
361-
OSError(errno, strerror[, filename[, winerror[, filename2]]])
360+
.. exception:: OSError([[errno,] strerror,] /, filename=None, winerror=None, filename2=None)
362361

363362
.. index:: pair: module; errno
364363

365364
This exception is raised when a system function returns a system-related
366365
error, including I/O failures such as "file not found" or "disk full"
367366
(not for illegal argument types or other incidental errors).
368367

369-
The second form of the constructor sets the corresponding attributes,
370-
described below. The attributes default to :const:`None` if not
371-
specified. For backwards compatibility, if three arguments are passed,
372-
the :attr:`~BaseException.args` attribute contains only a 2-tuple
373-
of the first two constructor arguments.
368+
The constructor arguments set the corresponding attributes, described below.
369+
If *errno* is omitted,
370+
it defaults to the error code which corresponds to the exception class,
371+
for the subclasses listed in `OS exceptions`_ below,
372+
and to ``None`` for :exc:`OSError` itself.
373+
If *strerror* is omitted,
374+
it is derived from *winerror* on Windows when that was given,
375+
and from the resulting :attr:`.errno` otherwise.
376+
The remaining attributes default to ``None``.
377+
378+
If *filename*, *winerror* or *filename2* is given,
379+
the :attr:`~BaseException.args` attribute is set to
380+
``(errno, strerror, filename, winerror, filename2)``,
381+
truncated after the last of the three which was given,
382+
with omitted values replaced by ``None``.
383+
Otherwise it contains the arguments as passed.
384+
Either way ``type(exc)(*exc.args)`` reproduces the exception,
385+
which is how it is pickled.
374386

375387
The constructor often actually returns a subclass of :exc:`OSError`, as
376388
described in `OS exceptions`_ below. The particular subclass depends on
@@ -380,7 +392,9 @@ The following exceptions are the exceptions that are usually raised.
380392

381393
.. attribute:: errno
382394

383-
A numeric error code from the C variable :c:data:`errno`.
395+
A numeric error code from the C variable :c:data:`errno`,
396+
or the :attr:`default_errno` of the exception class
397+
when the constructor was called without one.
384398

385399
.. attribute:: winerror
386400

@@ -423,6 +437,11 @@ The following exceptions are the exceptions that are usually raised.
423437
:term:`filesystem encoding and error handler`. Also, the *filename2*
424438
constructor argument and attribute was added.
425439

440+
.. versionchanged:: next
441+
*errno* and *strerror* can now be omitted.
442+
*filename*, *winerror* and *filename2* can be passed by keyword.
443+
The :attr:`~BaseException.args` attribute is no longer truncated.
444+
426445

427446
.. exception:: OverflowError
428447

@@ -737,6 +756,20 @@ OS exceptions
737756

738757
The following exceptions are subclasses of :exc:`OSError`, they get raised
739758
depending on the system error code.
759+
Each of them corresponds to one or more :mod:`errno` values,
760+
and defines the first of them as a class attribute:
761+
762+
.. attribute:: OSError.default_errno
763+
764+
The error code which corresponds to the exception class,
765+
used as :attr:`~OSError.errno` when the *errno* argument is omitted.
766+
It is not defined by :exc:`OSError` itself,
767+
nor by :exc:`ConnectionError`,
768+
which correspond to no single error code.
769+
A user-defined subclass may define it
770+
to give its instances a default :attr:`~OSError.errno` too.
771+
772+
.. versionadded:: next
740773

741774
.. exception:: BlockingIOError
742775

@@ -754,6 +787,12 @@ depending on the system error code.
754787
before it blocked. This attribute is available when using the
755788
buffered I/O classes from the :mod:`io` module.
756789

790+
It is set by passing the *characters_written* keyword argument
791+
or the third positional argument.
792+
793+
.. versionchanged:: next
794+
Added the *characters_written* keyword argument.
795+
757796
.. exception:: ChildProcessError
758797

759798
Raised when an operation on a child process failed.

Doc/whatsnew/3.16.rst

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,23 @@ Other language changes
8585
libraries.
8686
(Contributed by Serhiy Storchaka in :gh:`78959`.)
8787

88+
* The :exc:`OSError` constructor no longer requires the *errno* and
89+
*strerror* arguments.
90+
If *errno* is omitted, it defaults to the error code which corresponds
91+
to the exception class, so that ``FileNotFoundError()`` works both with
92+
code which tests the :attr:`~OSError.errno` attribute and with code
93+
which uses :func:`isinstance`.
94+
If *strerror* is omitted, it is derived from the resulting
95+
:attr:`~OSError.errno`, or on Windows from :attr:`~OSError.winerror`
96+
if that was given.
97+
The *filename*, *winerror* and *filename2* arguments can now be passed
98+
by keyword, as can *characters_written* for :exc:`BlockingIOError`::
99+
100+
>>> str(FileNotFoundError(filename='cfg.ini'))
101+
"[Errno 2] No such file or directory: 'cfg.ini'"
102+
103+
(Contributed by Serhiy Storchaka in :gh:`109714`.)
104+
88105
* :ref:`Frame objects <frame-objects>` now support :mod:`weak references
89106
<weakref>`. This allows associating extra data with active frames,
90107
for example in debuggers, without keeping the frames (and everything
@@ -845,6 +862,15 @@ that may require changes to your code.
845862
:exc:`TypeError`.
846863
(Contributed by Serhiy Storchaka in :gh:`152587`.)
847864

865+
* The :attr:`~BaseException.args` attribute of :exc:`OSError` is no longer
866+
truncated to two items when a file name is given, so that
867+
``OSError(2, 'No such file or directory', 'cfg.ini').args`` is now the
868+
whole 3-tuple. This makes an exception which carries a file name
869+
survive pickling.
870+
Passing more than five positional arguments to the constructor now
871+
raises :exc:`TypeError` instead of being silently ignored.
872+
(Contributed by Serhiy Storchaka in :gh:`109714`.)
873+
848874
* On Windows, seeking a pipe now fails instead of silently appearing to
849875
succeed: :func:`os.lseek` and :meth:`~io.IOBase.seek` raise :exc:`OSError`,
850876
and :meth:`~io.IOBase.seekable` returns ``False``. As a consequence,

Include/internal/pycore_pyerrors.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,12 @@ PyAPI_FUNC(void) _PyErr_SetString(
136136
PyObject *exception,
137137
const char *string);
138138

139+
#ifdef MS_WINDOWS
140+
/* Return the message for a Windows error code as a new reference,
141+
or NULL with an exception set. */
142+
extern PyObject* _PyErr_WindowsErrorMessage(unsigned long err);
143+
#endif
144+
139145
/*
140146
* Set an exception with the error message decoded from the current locale
141147
* encoding (LC_CTYPE).

Lib/test/test_capi/test_exceptions.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ def test_set_object(self):
228228
# is superclass, so does not wrap
229229
with self.assertRaises(PermissionError) as e:
230230
_testcapi.exc_set_object(OSError, PermissionError(24))
231-
self.assertEqual(e.exception.args, (24,))
231+
self.assertEqual(e.exception.args, (errno.EACCES, 24))
232232

233233
class Meta(type):
234234
def __subclasscheck__(cls, sub):
@@ -305,7 +305,7 @@ def test_setfromerrnowithfilename(self):
305305
with self.assertRaises(FileNotFoundError) as e:
306306
setfromerrnowithfilename(ENOENT, OSError, b'file')
307307
self.assertEqual(e.exception.args,
308-
(ENOENT, 'No such file or directory'))
308+
(ENOENT, 'No such file or directory', 'file'))
309309
self.assertEqual(e.exception.errno, ENOENT)
310310
self.assertEqual(e.exception.filename, 'file')
311311

@@ -325,7 +325,7 @@ def test_setfromerrnowithfilename(self):
325325

326326
with self.assertRaises(OSError) as e:
327327
setfromerrnowithfilename(0, OSError, b'file')
328-
self.assertEqual(e.exception.args, (0, 'Error'))
328+
self.assertEqual(e.exception.args, (0, 'Error', 'file'))
329329
self.assertEqual(e.exception.errno, 0)
330330
self.assertEqual(e.exception.filename, 'file')
331331

Lib/test/test_concurrent_futures/test_as_completed.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def test_correct_timeout_exception_msg(self):
108108
with self.assertRaises(futures.TimeoutError) as cm:
109109
list(futures.as_completed(futures_list, timeout=0))
110110

111-
self.assertEqual(str(cm.exception), '2 (of 4) futures unfinished')
111+
self.assertIn('2 (of 4) futures unfinished', str(cm.exception))
112112

113113

114114
create_executor_tests(globals(), AsCompletedTests)

Lib/test/test_exception_hierarchy.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,22 @@ def test_errno_mapping(self):
9595
e = OSError(errcode, "Some message")
9696
self.assertIs(type(e), OSError, repr(e))
9797

98+
def _defaults(self):
99+
# The first errno listed for a class is the one it defaults to.
100+
defaults = {}
101+
for errcode, exc in self._map.items():
102+
defaults.setdefault(exc, errcode)
103+
return defaults
104+
105+
def test_default_errno(self):
106+
for exc, errcode in self._defaults().items():
107+
with self.subTest(exc=exc.__name__):
108+
self.assertEqual(exc.default_errno, errcode)
109+
e = exc()
110+
self.assertEqual(e.errno, errcode)
111+
self.assertEqual(e.strerror, os.strerror(errcode))
112+
self.assertEqual(e.args, (errcode, os.strerror(errcode)))
113+
98114
def test_try_except(self):
99115
filename = "some_hopefully_non_existing_file"
100116

@@ -136,6 +152,80 @@ def test_posix_error(self):
136152
if os.name == "nt":
137153
self.assertEqual(e.winerror, None)
138154

155+
def test_strerror_derived_from_errno(self):
156+
e = FileNotFoundError()
157+
self.assertEqual(e.errno, errno.ENOENT)
158+
self.assertEqual(e.strerror, os.strerror(errno.ENOENT))
159+
# an explicit strerror wins over the one derived from errno
160+
e = FileNotFoundError('not found')
161+
self.assertEqual(e.errno, errno.ENOENT)
162+
self.assertEqual(e.strerror, 'not found')
163+
self.assertEqual(e.args, (errno.ENOENT, 'not found'))
164+
# ... including an explicit None
165+
e = FileNotFoundError(errno.ENOENT, None)
166+
self.assertIsNone(e.strerror)
167+
168+
@unittest.skipUnless(os.name == "nt", "Windows-specific test")
169+
def test_strerror_derived_from_winerror(self):
170+
# the message of the Windows error code is more specific than the one
171+
# of the errno it is translated to
172+
import ctypes
173+
e = OSError(filename="foo.txt", winerror=183)
174+
self.assertEqual(e.errno, EEXIST)
175+
expected = ctypes.WinError(183).strerror.rstrip(" .")
176+
self.assertEqual(e.strerror, expected)
177+
# a code which the system has no message for
178+
e = OSError(winerror=99999)
179+
self.assertEqual(e.errno, errno.EINVAL)
180+
self.assertEqual(e.strerror, "Windows Error 0x%x" % 99999)
181+
182+
def test_strerror_not_derived_from_bogus_errno(self):
183+
for code in 2**31, -2**31-1, 2**1000, -2**1000, 'x':
184+
with self.subTest(default_errno=code):
185+
cls = type('E', (OSError,), {'default_errno': code})
186+
e = cls()
187+
self.assertEqual(e.errno, code)
188+
self.assertIsNone(e.strerror)
189+
190+
@unittest.skipUnless(os.name == "nt", "Windows-specific test")
191+
def test_strerror_not_derived_from_bogus_winerror(self):
192+
# a winerror out of the C long range is rejected
193+
for code in 2**31, -2**31-1, 2**1000, -2**1000:
194+
with self.subTest(winerror=code):
195+
self.assertRaises(OverflowError, OSError, winerror=code)
196+
# a winerror which is not an integer is not translated at all
197+
e = OSError(winerror='x')
198+
self.assertIsNone(e.errno)
199+
self.assertIsNone(e.strerror)
200+
self.assertEqual(e.winerror, 'x')
201+
202+
def test_keyword_arguments(self):
203+
e = FileNotFoundError(filename='foo.txt')
204+
self.assertEqual(e.errno, errno.ENOENT)
205+
self.assertEqual(e.strerror, os.strerror(errno.ENOENT))
206+
self.assertEqual(e.filename, 'foo.txt')
207+
self.assertIsNone(e.filename2)
208+
209+
e = OSError('cannot open', filename='foo.txt', filename2='bar.txt')
210+
self.assertIsNone(e.errno)
211+
self.assertEqual(e.strerror, 'cannot open')
212+
self.assertEqual(e.filename, 'foo.txt')
213+
self.assertEqual(e.filename2, 'bar.txt')
214+
215+
e = OSError(EEXIST, 'exists', filename='foo.txt')
216+
self.assertEqual(e.errno, EEXIST)
217+
self.assertEqual(e.filename, 'foo.txt')
218+
219+
def test_keyword_argument_errors(self):
220+
# errno and strerror are positional-only
221+
self.assertRaises(TypeError, OSError, errno=EEXIST)
222+
self.assertRaises(TypeError, OSError, strerror='exists')
223+
# and cannot be given twice
224+
self.assertRaises(TypeError, OSError,
225+
EEXIST, 'exists', 'foo.txt', filename='bar.txt')
226+
# more than five arguments
227+
self.assertRaises(TypeError, OSError, 1, 2, 3, 4, 5, 6)
228+
139229
@unittest.skipUnless(os.name == "nt", "Windows-specific test")
140230
def test_errno_translation(self):
141231
# ERROR_ALREADY_EXISTS (183) -> EEXIST
@@ -145,6 +235,13 @@ def test_errno_translation(self):
145235
self.assertEqual(e.args[0], EEXIST)
146236
self.assertEqual(e.strerror, "File already exists")
147237
self.assertEqual(e.filename, "foo.txt")
238+
# winerror can also be given by keyword
239+
e = OSError("File already exists", filename="foo.txt", winerror=183)
240+
self.assertEqual(e.winerror, 183)
241+
self.assertEqual(e.errno, EEXIST)
242+
self.assertEqual(e.args, (EEXIST, "File already exists", "foo.txt", 183))
243+
self.assertEqual(e.strerror, "File already exists")
244+
self.assertEqual(e.filename, "foo.txt")
148245

149246
def test_blockingioerror(self):
150247
args = ("a", "b", "c", "d", "e")
@@ -162,6 +259,26 @@ def test_blockingioerror(self):
162259
with self.assertRaises(AttributeError):
163260
e.characters_written
164261

262+
# characters_written can also be given by keyword
263+
e = BlockingIOError("would block", characters_written=3)
264+
self.assertEqual(e.strerror, "would block")
265+
self.assertEqual(e.characters_written, 3)
266+
# including when the class is chosen by errno
267+
e = OSError(errno.EAGAIN, "would block", characters_written=3)
268+
self.assertIs(type(e), BlockingIOError)
269+
self.assertEqual(e.characters_written, 3)
270+
# but only for BlockingIOError
271+
for cls in OSError, FileNotFoundError:
272+
with self.subTest(cls=cls.__name__):
273+
self.assertRaises(TypeError, cls, characters_written=3)
274+
# and not together with a file name, which it is an alternative to
275+
self.assertRaises(TypeError, BlockingIOError,
276+
filename="foo.txt", characters_written=3)
277+
self.assertRaises(TypeError, BlockingIOError,
278+
errno.EAGAIN, "would block", 3, characters_written=3)
279+
# and it is keyword-only
280+
self.assertRaises(TypeError, BlockingIOError, 1, 2, 3, 4, 5, 6)
281+
165282

166283
class ExplicitSubclassingTest(unittest.TestCase):
167284

0 commit comments

Comments
 (0)