os.open

The layer under the built-in open(): os.open returns a plain int, os.read/os.write move bytes, os.lseek moves the position, os.close frees it. Nothing is buffered and nothing is closed for you. On Windows add os.O_BINARY, or newlines are translated. Available on Unix and Windows; SEEK_DATA / SEEK_HOLE only on Linux 3.1+, macOS and other Unix.

os functionPython 3.0+ (dir_fd 3.3+, non-inheritable fds 3.4+)
Common call
fd = os.open('f.bin', os.O_WRONLY | os.O_CREAT | os.O_TRUNC)
Returns
int file descriptor — always os.close(fd) it
Replaces
open() when you need exact OS flags such as O_EXCL
Watch out
Windows opens in text mode unless you add os.O_BINARY
os.open(pathpath — The file to open (path-like objects accepted since 3.6).type: str | bytes | PathLike · required, flagsflags — Exactly one of O_RDONLY / O_WRONLY / O_RDWR, OR-ed with extras such as O_CREAT, O_TRUNC, O_APPEND, O_EXCL (and O_BINARY on Windows).type: int · required, modemode — Permission bits for a newly created file; the umask is masked out first. Ignored when the file already exists.type: int · default: 0o777=0o777, *, dir_fddir_fd — Open path relative to this directory descriptor (3.3+). Unix only: on Windows os.open is not in os.supports_dir_fd.type: int | None · default: None=None)
→ int

Parameters

NameTypeRequiredDescription
pathstr | bytes | PathLikeyesThe file to open (path-like objects accepted since 3.6).
flagsintyesExactly one of O_RDONLY / O_WRONLY / O_RDWR, OR-ed with extras such as O_CREAT, O_TRUNC, O_APPEND, O_EXCL (and O_BINARY on Windows).
modeintno (0o777)Permission bits for a newly created file; the umask is masked out first. Ignored when the file already exists.
dir_fdint | Noneno (None)Open path relative to this directory descriptor (3.3+). Unix only: on Windows os.open is not in os.supports_dir_fd.

Return value

int — A new file descriptor (a small integer, non-inheritable). Close it with os.close().

Common patterns

Create a file only if it does not exist
O_CREAT | O_EXCL is atomic: exactly one process wins, the others get FileExistsError.
import os
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, 'O_BINARY', 0)
try:
    fd = os.open('app.lock', flags, 0o644)
except FileExistsError:
    print('already running')
else:
    with os.fdopen(fd, 'w') as f:
        f.write(str(os.getpid()))
Raw fd in, file object out
Open with exact flags, then let os.fdopen handle buffering, encoding and closing.
import os
fd = os.open('secret.txt', os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(fd, 'w', encoding='utf-8') as f:
    f.write('token')
Read everything from a descriptor
os.read returns at most n bytes; loop until it returns b"".
import os
def read_all(fd, chunk=65536):
    parts = []
    while True:
        data = os.read(fd, chunk)
        if not data:
            return b''.join(parts)
        parts.append(data)
Find the data regions of a sparse file (Linux, macOS)
SEEK_DATA jumps to the next byte that holds data, SEEK_HOLE to the next hole.
import os
fd = os.open('disk.img', os.O_RDONLY)
try:
    start = os.lseek(fd, 0, os.SEEK_DATA)
    end = os.lseek(fd, start, os.SEEK_HOLE)
finally:
    os.close(fd)

Examples

1. Write bytes, then read them back
import os B = getattr(os, 'O_BINARY', 0) fd = os.open('data.bin', os.O_WRONLY | os.O_CREAT | os.O_TRUNC | B) try: n = os.write(fd, b'hello') finally: os.close(fd) fd = os.open('data.bin', os.O_RDONLY | B) try: data = os.read(fd, 100) finally: os.close(fd) (n, data)
Returns
(5, b'hello')
2. os.read returns b"" at end of file
import os from pathlib import Path Path('f.bin').write_bytes(b'abc') fd = os.open('f.bin', os.O_RDONLY | getattr(os, 'O_BINARY', 0)) try: chunks = [os.read(fd, 2), os.read(fd, 2), os.read(fd, 2)] finally: os.close(fd) chunks
Returns
[b'ab', b'c', b'']
3. lseek returns the new position
import os from pathlib import Path Path('f.bin').write_bytes(b'hello world') fd = os.open('f.bin', os.O_RDONLY | getattr(os, 'O_BINARY', 0)) try: size = os.lseek(fd, 0, os.SEEK_END) pos = os.lseek(fd, 6, os.SEEK_SET) tail = os.read(fd, 5) here = os.lseek(fd, 0, os.SEEK_CUR) finally: os.close(fd) (size, pos, tail, here)
Returns
(11, 6, b'world', 11)
4. The SEEK constants are 0, 1, 2 (shared with io)
import io, os (os.SEEK_SET, os.SEEK_CUR, os.SEEK_END, os.SEEK_END == io.SEEK_END)
Returns
(0, 1, 2, True)
5. O_EXCL refuses to open an existing file
import os flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL os.close(os.open('lock', flags)) try: os.close(os.open('lock', flags)) except OSError as e: result = type(e).__name__ result
Returns
'FileExistsError'
6. fdopen wraps the fd; closing the file closes the fd
import os fd = os.open('note.txt', os.O_WRONLY | os.O_CREAT) with os.fdopen(fd, 'w') as f: f.write('ok') try: os.close(fd) except OSError as e: result = (type(e).__name__, open('note.txt').read()) result
Returns
('OSError', 'ok')
7. New descriptors are not inherited by child processes
import os fd = os.open('f', os.O_WRONLY | os.O_CREAT) try: result = (type(fd).__name__, os.get_inheritable(fd)) finally: os.close(fd) result
Returns
('int', False)

Pitfalls

1. Overwriting without O_TRUNC
O_WRONLY alone writes over the start of the file and keeps the rest of the old content. Add O_TRUNC to replace the file.
O_WRONLY
import os
from pathlib import Path
Path('f.txt').write_bytes(b'hello world')
fd = os.open('f.txt', os.O_WRONLY)
try:
    os.write(fd, b'HI')
finally:
    os.close(fd)
Path('f.txt').read_bytes()
b'HIllo world'
O_WRONLY | O_TRUNC
import os
from pathlib import Path
Path('f.txt').write_bytes(b'hello world')
fd = os.open('f.txt', os.O_WRONLY | os.O_TRUNC)
try:
    os.write(fd, b'HI')
finally:
    os.close(fd)
Path('f.txt').read_bytes()
b'HI'
2. Opening a missing file without O_CREAT
os.open never creates a file unless the flags say so; open(path, "w") does that for you, os.open does not.
O_WRONLY
import os
try:
    os.open('new.txt', os.O_WRONLY)
except OSError as e:
    result = type(e).__name__
result
'FileNotFoundError'
O_WRONLY | O_CREAT
import os
fd = os.open('new.txt', os.O_WRONLY | os.O_CREAT)
os.close(fd)
os.path.exists('new.txt')
True
3. Passing str to os.write
Descriptors carry bytes only. Encode the text (or use os.fdopen to get a text file object).
str
import os
fd = os.open('t.txt', os.O_WRONLY | os.O_CREAT)
try:
    os.write(fd, 'hi')
finally:
    os.close(fd)
TypeError: a bytes-like object is required, not 'str'
encode
import os
fd = os.open('t.txt', os.O_WRONLY | os.O_CREAT)
try:
    n = os.write(fd, 'hi'.encode('utf-8'))
finally:
    os.close(fd)
n
2

When to use

Use it
  • You need flags open() does not expose: O_EXCL with a mode, O_NOFOLLOW, O_CLOEXEC, O_TMPFILE, dir_fd
  • Working with descriptors from pipe(), dup(), sockets or a parent process
  • Unbuffered byte-level I/O where every os.write is one system call
Reach for something else
  • Ordinary file reading and writing → the built-in open() (buffered, encodings, with-block closing)
  • Exclusive creation only → open(path, "x") already fails if the file exists
  • Closing a file object → f.close(), not os.close(f.fileno())

Notes

CPython impl
os.open / os.read / os.write / os.lseek / os.close are thin wrappers over the C runtime calls in Modules/posixmodule.c; os.fdopen is the built-in open() with an int as first argument (Lib/os.py)
Windows text mode
Without os.O_BINARY, Windows opens the fd in text mode: os.write(fd, b'a\nb') returns 3 but puts 4 bytes (a, CR, LF, b) on disk, and os.read turns CR LF back into LF. On Linux and macOS there is no text mode and O_BINARY does not exist — use getattr(os, 'O_BINARY', 0)
Inheritance
Since 3.4 every new fd from os.open is non-inheritable (PEP 446): child processes do not get it unless you call os.set_inheritable(fd, True)
closerange
os.closerange(lo, hi) closes every fd from lo up to hi - 1 and silently ignores ones that are not open. Never run it over a range you do not own: it also closes descriptors used by the interpreter or libraries
SEEK_DATA / SEEK_HOLE
Unix only (Linux 3.1+, macOS): jump to the next data region or the next hole of a sparse file.
Errors
Every call raises OSError subclasses: FileNotFoundError, FileExistsError, PermissionError; a closed or invalid fd gives OSError with errno.EBADF (9)

FAQ

File descriptor numbers depend on what else the process has open, and text-mode handling differs on Windows, so the examples never show an fd and print only the bytes that were written and read back.