8.4 Predefined Streams, Handles vs. Streams, and errno Diagnostics
Key Takeaways
- Python opens three streams before your code runs: `sys.stdin` (descriptor 0, read-only text), `sys.stdout` (descriptor 1, write-only text), and `sys.stderr` (descriptor 2, write-only text) — none of them requires or accepts an `open()` call.
- A **handle** is the small integer file descriptor the operating system assigns, retrievable with `stream.fileno()`; a **stream** is the buffered Python object wrapped around that handle, and all file methods operate on the stream.
- `errno` values are numeric OS error codes: `ENOENT` is 2, `EPERM` is 1, `EACCES` is 13, `EEXIST` is 17, `ENOTDIR` is 20, `EISDIR` is 21, `EMFILE` is 24, and `ENOSPC` is 28.
- Every `OSError` carries `.errno`, `.strerror`, and `.filename` attributes, and `os.strerror(code)` converts a numeric code into its human-readable message.
- Since Python 3.3, `IOError` is an alias of `OSError`, and the concrete subclasses (`FileNotFoundError`, `PermissionError`, `FileExistsError`, `IsADirectoryError`) map directly onto the corresponding `errno` values.
Predefined Streams, Handles vs. Streams, and errno Diagnostics
Objective 5.4 asks for input/output terminology and objective 5.5 names "the errno variable and its values". Those two items are the vocabulary layer beneath open() and read(): what a stream actually is, which streams already exist when your program starts, and how the operating system reports failures back into Python.
1. Handles Versus Streams
The syllabus asks you to distinguish a handle from a stream, and the distinction is architectural rather than cosmetic.
- A handle (also called a file descriptor) is the low-level token the operating system hands back when a file is opened. On POSIX systems it is a small non-negative integer. The handle carries no buffering, no encoding, and no convenience methods; it is just a key into the kernel's table of open files.
- A stream is the Python object that wraps a handle.
open()returns a stream, and it is the stream that providesread(),readline(),write(),seek(),tell(), andclose(), plus buffering and — in text mode — character encoding and decoding.
You can always recover the handle from a stream:
with open("data.txt", "w") as stream:
print(type(stream).__name__) # TextIOWrapper -> the stream object
print(stream.fileno()) # 3 (or similar) -> the OS handle
The mental model that answers most exam items: your program talks to a stream; the stream talks to a handle; the handle talks to the operating system. A "file object" and a "stream" are the same thing in Python terminology.
2. The Three Predefined Streams
Python opens three streams automatically at interpreter start-up. They live in the sys module and are already connected when the first line of your program runs — calling open() on them is neither required nor possible.
| Stream | Descriptor | Direction | Default target | Typical use |
|---|---|---|---|---|
sys.stdin | 0 | Read-only | Keyboard | Input consumed by input() |
sys.stdout | 1 | Write-only | Console | Normal program output from print() |
sys.stderr | 2 | Write-only | Console | Diagnostics, warnings, tracebacks |
import sys
print(sys.stdin.fileno(), sys.stdout.fileno(), sys.stderr.fileno())
# 0 1 2
print(sys.stdout.writable(), sys.stdout.readable())
# True False
print(sys.stdin.writable())
# False
All three are opened in text mode, so they carry an encoding (sys.stdout.encoding is typically 'utf-8') and exchange str, not bytes.
Why stderr Exists Separately
print() writes to sys.stdout by default. Diagnostics belong on sys.stderr so that a user redirecting normal output to a file still sees errors on the console, and so that a downstream pipeline is not polluted by warning text:
import sys
print("result: 42") # -> stdout
print("warning: cache miss", file=sys.stderr) # -> stderr
sys.stderr.write("fatal: aborting\n") # -> stderr, no newline added
Two behavioural details matter:
print(..., file=stream)appends theendstring (a newline by default);stream.write(...)appends nothing, so you must supply\nyourself.stream.write()returns the number of characters written, which is whysys.stdout.write("hi")at an interactive prompt echoeshifollowed by2.
Because stderr is typically unbuffered or line-buffered while stdout is block-buffered when redirected, interleaved output can appear out of order in a redirected log. Calling sys.stdout.flush() forces the pending buffer out.
3. The errno Variable and Its Values
When a system call fails, the operating system reports a numeric error code. Python surfaces it through the errno module and through the .errno attribute of every OSError.
import errno, os
print(errno.ENOENT, os.strerror(errno.ENOENT)) # 2 No such file or directory
print(errno.EACCES, os.strerror(errno.EACCES)) # 13 Permission denied
The codes named most often in Python I/O material:
| Constant | Value | Meaning |
|---|---|---|
errno.EPERM | 1 | Operation not permitted |
errno.ENOENT | 2 | No such file or directory |
errno.EACCES | 13 | Permission denied |
errno.EEXIST | 17 | File exists |
errno.ENOTDIR | 20 | Not a directory |
errno.EISDIR | 21 | Is a directory |
errno.EMFILE | 24 | Too many open files |
errno.ENOSPC | 28 | No space left on device |
os.strerror(code) turns any of these integers into its message string. Never hard-code the message text; derive it from the code.
4. Reading errno Off a Raised Exception
Every OSError instance exposes three diagnostic attributes:
import errno
try:
with open("/definitely/not/here.txt") as f:
data = f.read()
except OSError as exc:
print(type(exc).__name__) # FileNotFoundError
print(exc.errno) # 2
print(exc.strerror) # No such file or directory
print(exc.filename) # /definitely/not/here.txt
print(exc.errno == errno.ENOENT) # True
.errno— the numeric code.strerror— the OS message for that code.filename— the path that triggered the failure (Nonewhen the operation was not path-based)
IOError still exists but is a plain alias of OSError since Python 3.3, so IOError is OSError evaluates to True and catching one catches the other.
5. errno Codes Versus OSError Subclasses
Python 3.3 introduced concrete exception subclasses that already encode the common errno values, so the older "catch OSError, then branch on exc.errno" pattern is rarely needed:
errno value | Automatically raised subclass |
|---|---|
ENOENT (2) | FileNotFoundError |
EPERM (1) / EACCES (13) | PermissionError |
EEXIST (17) | FileExistsError |
EISDIR (21) | IsADirectoryError |
ENOTDIR (20) | NotADirectoryError |
# Legacy style - still correct, still testable
try:
stream = open("config.ini")
except OSError as exc:
if exc.errno == errno.ENOENT:
print("Missing file")
elif exc.errno == errno.EACCES:
print("No permission")
else:
raise
# Modern equivalent
try:
stream = open("config.ini")
except FileNotFoundError:
print("Missing file")
except PermissionError:
print("No permission")
Both forms appear in exam items. Recognise the numeric codes, and remember that every one of these subclasses still carries a populated .errno — the subclass is a convenience layer, not a replacement for the code.
Exam Trap:
errnois a module of integer constants, not an attribute of the file object. There is nostream.errno; the code lives on the raised exception (exc.errno) and on theerrnomodule (errno.ENOENT).
Which statement correctly describes the three predefined streams in Python?
What is the numeric value of errno.ENOENT, and which exception subclass does Python raise automatically when a system call fails with that code?
In Python terminology, what is the relationship between a handle and a stream?
Consider the following code:
What is the key behavioural difference between the two statements?import sys
sys.stderr.write("disk warning")
print("disk warning", file=sys.stderr)
You've completed this section
Continue exploring other exams