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.
Last updated: August 2026

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 provides read(), readline(), write(), seek(), tell(), and close(), 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.

StreamDescriptorDirectionDefault targetTypical use
sys.stdin0Read-onlyKeyboardInput consumed by input()
sys.stdout1Write-onlyConsoleNormal program output from print()
sys.stderr2Write-onlyConsoleDiagnostics, 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 the end string (a newline by default); stream.write(...) appends nothing, so you must supply \n yourself.
  • stream.write() returns the number of characters written, which is why sys.stdout.write("hi") at an interactive prompt echoes hi followed by 2.

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:

ConstantValueMeaning
errno.EPERM1Operation not permitted
errno.ENOENT2No such file or directory
errno.EACCES13Permission denied
errno.EEXIST17File exists
errno.ENOTDIR20Not a directory
errno.EISDIR21Is a directory
errno.EMFILE24Too many open files
errno.ENOSPC28No 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 (None when 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 valueAutomatically 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: errno is a module of integer constants, not an attribute of the file object. There is no stream.errno; the code lives on the raised exception (exc.errno) and on the errno module (errno.ENOENT).

Loading diagram...
From Python Stream to OS Handle, and the errno Path Back
Test Your Knowledge

Which statement correctly describes the three predefined streams in Python?

A
B
C
D
Test Your Knowledge

What is the numeric value of errno.ENOENT, and which exception subclass does Python raise automatically when a system call fails with that code?

A
B
C
D
Test Your Knowledge

In Python terminology, what is the relationship between a handle and a stream?

A
B
C
D
Test Your Knowledge

Consider the following code:

import sys

sys.stderr.write("disk warning")
print("disk warning", file=sys.stderr)
What is the key behavioural difference between the two statements?

A
B
C
D
Congratulations!

You've completed this section

Continue exploring other exams