https://github.com/python/cpython/commit/7b6c248d61808b787f09ed3d05e4c233a5841a74
commit: 7b6c248d61808b787f09ed3d05e4c233a5841a74
branch: main
author: Bénédikt Tran <[email protected]>
committer: hugovk <[email protected]>
date: 2026-05-06T17:41:26+03:00
summary:

gh-142307: deprecate legacy support for altering `IMAP4.file` (#142335)

Co-authored-by: Hugo van Kemenade <[email protected]>

files:
A Misc/NEWS.d/next/Library/2025-12-06-11-24-25.gh-issue-142307.w8evI9.rst
M Doc/deprecations/pending-removal-in-3.19.rst
M Doc/library/imaplib.rst
M Doc/whatsnew/3.15.rst
M Lib/imaplib.py
M Lib/test/test_imaplib.py

diff --git a/Doc/deprecations/pending-removal-in-3.19.rst 
b/Doc/deprecations/pending-removal-in-3.19.rst
index 044bb8a3934a2a..4a58c606ab7596 100644
--- a/Doc/deprecations/pending-removal-in-3.19.rst
+++ b/Doc/deprecations/pending-removal-in-3.19.rst
@@ -31,3 +31,12 @@ Pending removal in Python 3.19
   * :meth:`http.cookies.BaseCookie.js_output` is deprecated and will be
     removed in Python 3.19.
 
+* :mod:`imaplib`:
+
+  * Altering :attr:`IMAP4.file <imaplib.IMAP4.file>` is now deprecated
+    and slated for removal in Python 3.19. This property is now unused
+    and changing its value does not automatically close the current file.
+
+    Before Python 3.14, this property was used to implement the corresponding
+    ``read()`` and ``readline()`` methods for :class:`~imaplib.IMAP4` but this
+    is no longer the case since then.
diff --git a/Doc/library/imaplib.rst b/Doc/library/imaplib.rst
index b29b02d3cf5fe8..fabe2ca9127984 100644
--- a/Doc/library/imaplib.rst
+++ b/Doc/library/imaplib.rst
@@ -695,6 +695,16 @@ The following attributes are defined on instances of 
:class:`IMAP4`:
    .. versionadded:: 3.5
 
 
+.. property:: IMAP4.file
+
+   Internal :class:`~io.BufferedReader` associated with the underlying socket.
+   This property is documented for legacy purposes but not part of the public
+   interface. The caller is responsible to ensure that the current file is
+   closed before changing it.
+
+   .. deprecated-removed:: next 3.19
+
+
 .. _imap4-example:
 
 IMAP4 Example
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index 698a9f88e1ee39..2ca28378e6ef73 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -2107,6 +2107,13 @@ New deprecations
     (Contributed by kishorhange111 in :gh:`148849`.)
 
 
+* :mod:`imaplib`:
+
+  * Altering :attr:`IMAP4.file <imaplib.IMAP4.file>` is now deprecated
+    and slated for removal in Python 3.19. This property is now unused
+    and changing its value does *not* explicitly close the current file.
+
+
 * :mod:`re`:
 
   * :func:`re.match` and :meth:`re.Pattern.match` are now
diff --git a/Lib/imaplib.py b/Lib/imaplib.py
index cb3edceae0d9f1..2fafd9322c609e 100644
--- a/Lib/imaplib.py
+++ b/Lib/imaplib.py
@@ -313,25 +313,34 @@ def open(self, host='', port=IMAP4_PORT, timeout=None):
         self.host = host
         self.port = port
         self.sock = self._create_socket(timeout)
-        self._file = self.sock.makefile('rb')
-
+        # Since IMAP4 implements its own read() and readline() buffering,
+        # the '_imaplib_file' attribute is unused. Nonetheless it is kept
+        # and exposed solely for backward compatibility purposes.
+        self._imaplib_file = self.sock.makefile('rb')
 
     @property
     def file(self):
-        # The old 'file' attribute is no longer used now that we do our own
-        # read() and readline() buffering, with which it conflicts.
-        # As an undocumented interface, it should never have been accessed by
-        # external code, and therefore does not warrant deprecation.
-        # Nevertheless, we provide this property for now, to avoid suddenly
-        # breaking any code in the wild that might have been using it in a
-        # harmless way.
         import warnings
-        warnings.warn(
-            'IMAP4.file is unsupported, can cause errors, and may be removed.',
-            RuntimeWarning,
-            stacklevel=2)
-        return self._file
+        warnings._deprecated("IMAP4.file", remove=(3, 19))
+        return self._imaplib_file
 
+    @file.setter
+    def file(self, value):
+        import warnings
+        warnings._deprecated("IMAP4.file", remove=(3, 19))
+        # Ideally, we would want to close the previous file,
+        # but since we do not know how subclasses will use
+        # that setter, it is probably better to leave it to
+        # the caller.
+        self._imaplib_file = value
+
+    def _close_imaplib_file(self):
+        file = self._imaplib_file
+        if file is not None:
+            try:
+                file.close()
+            except OSError:
+                pass
 
     def read(self, size):
         """Read 'size' bytes from remote."""
@@ -417,7 +426,7 @@ def send(self, data):
 
     def shutdown(self):
         """Close I/O established in "open"."""
-        self._file.close()
+        self._close_imaplib_file()
         try:
             self.sock.shutdown(socket.SHUT_RDWR)
         except OSError as exc:
@@ -921,9 +930,10 @@ def starttls(self, ssl_context=None):
             ssl_context = ssl._create_stdlib_context()
         typ, dat = self._simple_command(name)
         if typ == 'OK':
+            self._close_imaplib_file()
             self.sock = ssl_context.wrap_socket(self.sock,
                                                 server_hostname=self.host)
-            self._file = self.sock.makefile('rb')
+            self._imaplib_file = self.sock.makefile('rb')
             self._tls_established = True
             self._get_capabilities()
         else:
@@ -1680,7 +1690,7 @@ def open(self, host=None, port=None, timeout=None):
         self.host = None        # For compatibility with parent class
         self.port = None
         self.sock = None
-        self._file = None
+        self._imaplib_file = None
         self.process = subprocess.Popen(self.command,
             bufsize=DEFAULT_BUFFER_SIZE,
             stdin=subprocess.PIPE, stdout=subprocess.PIPE,
diff --git a/Lib/test/test_imaplib.py b/Lib/test/test_imaplib.py
index cb5454b40eccf9..0b704d62655762 100644
--- a/Lib/test/test_imaplib.py
+++ b/Lib/test/test_imaplib.py
@@ -665,11 +665,33 @@ def test_control_characters(self):
 
     # property tests
 
-    def test_file_property_should_not_be_accessed(self):
+    def test_file_property_getter(self):
         client, _ = self._setup(SimpleIMAPHandler)
-        # the 'file' property replaced a private attribute that is now unsafe
-        with self.assertWarns(RuntimeWarning):
-            client.file
+        with self.assertWarns(DeprecationWarning):
+            self.assertIsInstance(client.file.raw, socket.SocketIO)
+
+    def test_file_property_setter(self):
+        client, _ = self._setup(SimpleIMAPHandler)
+        with self.assertWarns(DeprecationWarning):
+            # ensure that the caller closes the existing file
+            client.file.close()
+        for new_file in [mock.Mock(), None]:
+            with self.assertWarns(DeprecationWarning):
+                client.file = new_file
+            with self.assertWarns(DeprecationWarning):
+                self.assertIs(client.file, new_file)
+
+    def test_file_property_setter_should_not_close_previous_file(self):
+        client, _ = self._setup(SimpleIMAPHandler)
+        with mock.patch.object(client, "_imaplib_file", mock.Mock()) as f:
+            f.close.assert_not_called()
+            with self.assertWarns(DeprecationWarning):
+                self.assertIs(client.file, f)
+            with self.assertWarns(DeprecationWarning):
+                client.file = None
+            with self.assertWarns(DeprecationWarning):
+                self.assertIsNone(client.file)
+            f.close.assert_not_called()
 
 
 class NewIMAPTests(NewIMAPTestsMixin, unittest.TestCase):
diff --git 
a/Misc/NEWS.d/next/Library/2025-12-06-11-24-25.gh-issue-142307.w8evI9.rst 
b/Misc/NEWS.d/next/Library/2025-12-06-11-24-25.gh-issue-142307.w8evI9.rst
new file mode 100644
index 00000000000000..3c0eb0edcfba48
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-12-06-11-24-25.gh-issue-142307.w8evI9.rst
@@ -0,0 +1,4 @@
+:mod:`imaplib`: deprecate support for :attr:`IMAP4.file <imaplib.IMAP4.file>`.
+This attribute was never meant to be part of the public interface and altering
+its value may result in unclosed files or other synchronization issues with
+the underlying socket. Patch by Bénédikt Tran.

_______________________________________________
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