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:
openflags mapping: the Pythonmodestring ("r","w","a","x","+"","b") is mapped to theO_RDONLY,O_WRONLY,O_RDWR,O_CREAT,O_TRUNC,O_APPEND,O_EXCLflag set.- fd inheritance: every new file descriptor is opened with
O_CLOEXEC(or theFD_CLOEXECfcntl flag as a fallback) so that child processes do not accidentally inherit open files. - EINTR retry: the
_Py_EINTR_RETRYmacro wraps each system call in ado { ... } while (errno == EINTR && !(PyErr_CheckSignals()))loop to restart on signal interruption.
Map
| Lines | Symbol | Role | gopy |
|---|---|---|---|
| 1-200 | fileio struct, fileio_new, fileio_init / fileio_open | Type struct (fd, mode flags, closefd), constructor: parse mode string, call open(2), set O_CLOEXEC. | module/io/ |
| 200-400 | fileio_read, fileio_readall, fileio_readinto | Read path: single read(2) call inside _Py_EINTR_RETRY, readall in a loop growing a bytearray. | module/io/ |
| 400-600 | fileio_write | Write path: single write(2) call inside _Py_EINTR_RETRY, returns bytes written. | module/io/ |
| 600-900 | fileio_seek, fileio_tell, fileio_truncate, fileio_close, fileio_finalize, method table, FileIO_Type | Seek/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.