python/cpython · error · OSError

write() returned incorrect number of bytes

Error message

write() returned incorrect number of bytes

What it means

Raised by BufferedWriter._flush_unlocked when the raw stream's write() returns an out-of-range byte count — more than the supplied buffer length, or a negative number. Such a return value breaks the writer's bookkeeping (it does 'del self._write_buf[:n]'), so it is treated as a broken raw implementation and surfaced as OSError('write() returned incorrect number of bytes').

Solutions

  1. Make the custom raw write() return exactly the number of bytes actually written, in [0, len(b)].
  2. For would-block, return None (RawIOBase protocol) rather than -1.
  3. Add a unit test asserting 0 <= raw.write(b) <= len(b) for all paths.

Example fix

# before
class MyRaw(io.RawIOBase):
    def write(self, b):
        self.queue.append(b)
        return len(self.queue)  # wrong: total queued, can exceed len(b)

# after
class MyRaw(io.RawIOBase):
    def write(self, b):
        self.queue.append(b)
        return len(b)  # exactly what was accepted
Defensive patterns

Strategy: type-guard

Validate before calling

n = raw.write(chunk)
assert 0 <= n <= len(chunk), f"raw.write returned {n} for {len(chunk)} bytes"
del chunk[:n]

Type guard

def sane_write_count(raw, probe: bytes) -> bool:
    n = raw.write(probe)
    return n is None or 0 <= n <= len(probe)

Prevention

When it happens

Trigger: Custom raw classes that return len(data) unconditionally (even on short writes), return the total cached length instead of the written count, or return -1 error sentinels; rarely, wrapper math bugs computing n from multiple writes.

Common situations: Buggy hand-written RawIOBase subclasses; adapters over C libraries whose write returns bytes-consumed semantics that include buffered-elsewhere bytes; mocks/fakes in tests returning wrong counts.

Related errors


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

Appendix: source

Thrown at Lib/_pyio.py:1321

    def flush(self):
        with self._write_lock:
            self._flush_unlocked()

    def _flush_unlocked(self):
        if self.closed:
            raise ValueError("flush on closed file")
        while self._write_buf:
            try:
                n = self.raw.write(self._write_buf)
            except BlockingIOError:
                raise RuntimeError("self.raw should implement RawIOBase: it "
                                   "should not raise BlockingIOError")
            if n is None:
                raise BlockingIOError(
                    errno.EAGAIN,
                    "write could not complete without blocking", 0)
            if n > len(self._write_buf) or n < 0:
                raise OSError("write() returned incorrect number of bytes")
            del self._write_buf[:n]

    def tell(self):
        return _BufferedIOMixin.tell(self) + len(self._write_buf)

    def seek(self, pos, whence=0):
        if whence not in valid_seek_flags:
            raise ValueError("invalid whence value")
        with self._write_lock:
            self._flush_unlocked()
            return _BufferedIOMixin.seek(self, pos, whence)

    def close(self):
        with self._write_lock:
            if self.raw is None or self.closed:
                return
        # We have to release the lock and call self.flush() (which will
        # probably just re-take the lock) in case flush has been overridden in

View on GitHub (pinned to bc6749cc3b)