Skip to main content

Modules/_io/fileio.c

cpython 3.14 @ ab2d84fe1023/Modules/_io/fileio.c

fileio.c implements io.FileIO, the raw unbuffered layer at the bottom of the I/O stack. It wraps a single POSIX file descriptor and provides read, readall, readinto, write, seek, tell, and truncate methods that map directly to read(2), write(2), lseek(2), and ftruncate(2).

FileIO is not buffered. Each read or write call translates to exactly one system call (or one retry loop around EINTR). The buffered wrappers BufferedReader and BufferedWriter in bufferedio.c sit on top of FileIO and provide the larger read-ahead and write-behind buffers visible to application code.

Key responsibilities:

  • open flags mapping: the Python mode string ("r", "w", "a", "x", "+"", "b") is mapped to the O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_TRUNC, O_APPEND, O_EXCL flag set.
  • fd inheritance: every new file descriptor is opened with O_CLOEXEC (or the FD_CLOEXEC fcntl flag as a fallback) so that child processes do not accidentally inherit open files.
  • EINTR retry: the _Py_EINTR_RETRY macro wraps each system call in a do { ... } while (errno == EINTR && !(PyErr_CheckSignals())) loop to restart on signal interruption.

Map

LinesSymbolRolegopy
1-200fileio struct, fileio_new, fileio_init / fileio_openType struct (fd, mode flags, closefd), constructor: parse mode string, call open(2), set O_CLOEXEC.module/io/
200-400fileio_read, fileio_readall, fileio_readintoRead path: single read(2) call inside _Py_EINTR_RETRY, readall in a loop growing a bytearray.module/io/
400-600fileio_writeWrite path: single write(2) call inside _Py_EINTR_RETRY, returns bytes written.module/io/
600-900fileio_seek, fileio_tell, fileio_truncate, fileio_close, fileio_finalize, method table, FileIO_TypeSeek/tell/truncate wrapping lseek(2) and ftruncate(2); close that optionally closes the fd; type definition.module/io/

Reading

Open flags mapping (lines 1 to 200)

cpython 3.14 @ ab2d84fe1023/Modules/_io/fileio.c#L1-200

fileio_open converts the Python mode string into POSIX flags. The mapping is stricter than fopen(3): each character is checked individually and invalid combinations are rejected:

static int
fileio_init(PyObject *oself, PyObject *args, PyObject *kwds)
{
int flags = 0;
int readable = 0, writable = 0, appending = 0, creating = 0;

for (const char *s = mode; *s; s++) {
switch (*s) {
case 'r': readable = 1; break;
case 'w': writable = 1; break;
case 'a': appending = 1; break;
case 'x': creating = 1; break;
case 'b': break; /* binary: no-op for FileIO */
case '+': readable = writable = 1; break;
default:
PyErr_Format(PyExc_ValueError,
"invalid mode: '%.200s'", mode);
return -1;
}
}

if (readable && writable) flags = O_RDWR;
else if (readable) flags = O_RDONLY;
else flags = O_WRONLY;

if (creating) flags |= O_EXCL | O_CREAT;
if (writable && !appending && !creating)
flags |= O_CREAT | O_TRUNC;
if (appending) flags |= O_APPEND | O_CREAT;

#ifdef O_CLOEXEC
flags |= O_CLOEXEC;
#endif

do {
fd = open(name, flags, 0666);
} while (fd < 0 && errno == EINTR && !(PyErr_CheckSignals()));
...
}

After open succeeds, if O_CLOEXEC was not available at compile time, fileio_open sets FD_CLOEXEC with fcntl(fd, F_SETFD, FD_CLOEXEC) as a fallback. This matches CPython's policy that file descriptors opened by Python are close-on-exec by default (PEP 446).

When file is an integer rather than a path, FileIO accepts the existing fd directly and skips the open call. In that case, the closefd flag controls whether FileIO.close() will call close(2) on the fd.

_Py_EINTR_RETRY around syscalls (lines 200 to 600)

cpython 3.14 @ ab2d84fe1023/Modules/_io/fileio.c#L200-600

Every blocking system call in fileio.c is wrapped in _Py_EINTR_RETRY. The macro expands to a loop that retries on EINTR and checks for pending Python signals between retries:

/* From cpython/Include/cpython/fileutils.h */
#define _Py_EINTR_RETRY(result, expression) \
do { \
errno = 0; \
result = (expression); \
} while (result < 0 && errno == EINTR && \
!(PyErr_CheckSignals()))

Applied to read and write:

static PyObject *
fileio_read(fileio *self, PyObject *args)
{
Py_ssize_t n;
PyObject *bytes;
...
bytes = PyBytes_FromStringAndSize(NULL, size);
...
Py_BEGIN_ALLOW_THREADS
_Py_EINTR_RETRY(n, read(self->fd,
PyBytes_AS_STRING(bytes),
(size_t)size));
Py_END_ALLOW_THREADS

if (n < 0) {
Py_DECREF(bytes);
return PyErr_SetFromErrno(PyExc_OSError);
}
if (n != size)
_PyBytes_Resize(&bytes, n);
return bytes;
}

The GIL is released around the entire retry loop so that other threads can run during blocking I/O. PyErr_CheckSignals is safe to call from the main thread even without the GIL on CPython's reference implementation because it only reads Handlers[].tripped without modifying Python objects.

fileio_readall uses a different strategy: it calls fstat first to determine the file size, allocates a bytearray of that size, and fills it with a single read(2) call. If fstat is unavailable or the size is unknown (e.g. a pipe), it falls back to repeated read calls doubling a bytearray until EOF.

fileio_write follows the same pattern as fileio_read:

static PyObject *
fileio_write(fileio *self, PyObject *args)
{
Py_ssize_t n;
Py_buffer pbuf;
...
Py_BEGIN_ALLOW_THREADS
_Py_EINTR_RETRY(n, write(self->fd, pbuf.buf, (size_t)pbuf.len));
Py_END_ALLOW_THREADS

PyBuffer_Release(&pbuf);
if (n < 0)
return PyErr_SetFromErrno(PyExc_OSError);
return PyLong_FromSsize_t(n);
}

write may return fewer bytes than requested (short write). FileIO does not retry in that case; returning n < len(buf) is correct per POSIX, and BufferedWriter above it handles looping.

gopy mirror

module/io/ (pending). The Go port wraps an *os.File. read calls file.Read, write calls file.Write. EINTR is transparent in Go's runtime on Linux (Go syscalls restart automatically); on other platforms the retry loop is explicit. O_CLOEXEC is set by syscall.O_CLOEXEC when opening. The closefd flag is honored in FileIO.Close.

CPython 3.14 changes

O_CLOEXEC as the default for all Python-opened file descriptors was established by PEP 446 in Python 3.4. _Py_EINTR_RETRY replaced hand-written errno == EINTR loops in 3.5 as part of PEP 475. FileIO gained the opener keyword argument in 3.3, which allows a custom callable to replace open(2). The blksize attribute (for readall buffer sizing) was added in 3.12. Multi-phase init for _io was adopted in 3.12.