diff --git a/Lib/test/test_io/test_textio.py b/Lib/test/test_io/test_textio.py index 6e649240f08d5bf..9d3d5d120cc87b6 100644 --- a/Lib/test/test_io/test_textio.py +++ b/Lib/test/test_io/test_textio.py @@ -1097,6 +1097,31 @@ def test_illegal_encoder(self): t = self.TextIOWrapper(self.BytesIO(b'foo'), encoding="rot13") self.assertRaises(TypeError, t.write, 'bar') + def test_encoder_returns_str(self): + # gh-161518: an encoder returning its non-ASCII input str must be + # rejected, not have its internal storage written as bytes. + class Encoder(codecs.IncrementalEncoder): + def encode(self, input, final=False): + return input + + info = codecs.CodecInfo( + None, None, incrementalencoder=Encoder, + incrementaldecoder=codecs.getincrementaldecoder('utf-8'), + name='test_str_encoder') + search = lambda name: info if name == 'test_str_encoder' else None + codecs.register(search) + self.addCleanup(codecs.unregister, search) + + t = self.TextIOWrapper(self.BytesIO(), encoding='test_str_encoder') + self.assertRaises(TypeError, t.write, 'caf\xe9') + + # The ASCII fast path still works. + raw = self.BytesIO() + t = self.TextIOWrapper(raw, encoding='utf-8') + t.write('cafe') + t.flush() + self.assertEqual(raw.getvalue(), b'cafe') + def test_illegal_decoder(self): # Issue #17106 # Bypass the early encoding check added in issue 20404 diff --git a/Misc/NEWS.d/next/Library/2026-11-06-21-50-00.gh-issue-161518.Tx9Kq2.rst b/Misc/NEWS.d/next/Library/2026-11-06-21-50-00.gh-issue-161518.Tx9Kq2.rst new file mode 100644 index 000000000000000..435d58216fa118c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-11-06-21-50-00.gh-issue-161518.Tx9Kq2.rst @@ -0,0 +1,2 @@ +:class:`io.TextIOWrapper` now raises :exc:`TypeError`, instead of writing +corrupt output, when an encoder returns its non-ASCII input string. diff --git a/Modules/_io/textio.c b/Modules/_io/textio.c index b6f3f4f17119289..7dd28ace8c370fa 100644 --- a/Modules/_io/textio.c +++ b/Modules/_io/textio.c @@ -1831,7 +1831,7 @@ _io_TextIOWrapper_write_impl(textio *self, PyObject *text) Py_DECREF(text); if (b == NULL) return NULL; - if (b != text && !PyBytes_Check(b)) { + if (!PyBytes_Check(b) && !(b == text && PyUnicode_IS_ASCII(text))) { PyErr_Format(PyExc_TypeError, "encoder should return a bytes object, not '%.200s'", Py_TYPE(b)->tp_name);