python/cpython · error · RuntimeError

Cannot call writelines() after write_eof()

Error message

Cannot call writelines() after write_eof()

What it means

Raised by _SelectorSocketTransport.writelines() as RuntimeError when called after write_eof() has marked the transport as half-closed. writelines() is the batched variant of write() and obeys the same lifecycle rule: once the write side is shut down no further outgoing data is accepted.

Source

Thrown at Lib/asyncio/selector_events.py:1188

            if not self._buffer:
                self._loop._remove_writer(self._sock_fd)
                if self._empty_waiter is not None:
                    self._empty_waiter.set_result(None)
                if self._closing:
                    self._call_connection_lost(None)
                elif self._eof:
                    self._sock.shutdown(socket.SHUT_WR)

    def write_eof(self):
        if self._closing or self._eof:
            return
        self._eof = True
        if not self._buffer:
            self._sock.shutdown(socket.SHUT_WR)

    def writelines(self, list_of_data):
        if self._eof:
            raise RuntimeError('Cannot call writelines() after write_eof()')
        if self._empty_waiter is not None:
            raise RuntimeError('unable to writelines; sendfile is in progress')
        if not list_of_data:
            return

        if self._conn_lost:
            if self._conn_lost >= constants.LOG_THRESHOLD_FOR_CONNLOST_WRITES:
                logger.warning('socket.send() raised exception.')
            self._conn_lost += 1
            return

        for data in list_of_data:
            self._buffer.append(memoryview(data))
            self._buffer_size += len(data)
        self._write_ready()
        # If the entire buffer couldn't be written, register a write handler
        if self._buffer:
            self._add_writer(self._sock_fd, self._write_ready)

View on GitHub (pinned to bc6749cc3b)

Solutions

  1. Move writelines() before write_eof() so EOF is strictly the final transport operation.
  2. Guard writes with an `if transport.is_closing() or eof_sent: return` check in your protocol wrapper.
  3. Ensure producer tasks are done (await them) before half-closing the connection.

Example fix

// before
transport.write_eof()
transport.writelines([b"a\n", b"b\n"])  # RuntimeError

// after
transport.writelines([b"a\n", b"b\n"])
transport.write_eof()
Defensive patterns

Strategy: validation

Validate before calling

class WriteOnceEofTransport:
    def __init__(self, transport):
        self._t = transport
        self._eof = False
    def writelines(self, chunks):
        if self._eof or self._t.is_closing():
            return
        self._t.writelines(chunks)
    def write_eof(self):
        self._eof = True
        self._t.write_eof()

Try / catch

try:
    transport.writelines(chunks)
except RuntimeError as e:
    if 'after write_eof' in str(e):
        log.warning('dropped %d chunks after EOF', len(chunks))
    else:
        raise

Prevention

When it happens

Trigger: Calling transport.writelines([chunk1, chunk2]) or StreamWriter.writelines() after transport.write_eof() was already invoked on the same connection.

Common situations: Response-building code that calls write_eof() after finishing headers, then appends body chunks via writelines; shared handlers where one path half-closes early; refactoring write() loops into writelines() without revisiting the EOF ordering.

Related errors


AI-assisted analysis of python/cpython@bc6749cc3b (2026-08-14). Data as JSON: /api/errors/d644b2edf235b1c9. Report an issue: GitHub.