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.
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
| Name | Type | Required | Description |
|---|---|---|---|
| path | str | bytes | PathLike | yes | The file to open (path-like objects accepted since 3.6). |
| flags | int | yes | 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). |
| mode | int | no (0o777) | Permission bits for a newly created file; the umask is masked out first. Ignored when the file already exists. |
| dir_fd | int | None | no (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.