Study guide

8.4 Predefined Streams, Handles vs. Streams, and errno Diagnostics

OpenEDG PCAP — Certified Associate in Python Programming Exam

Track your progress as you read

Finished sections get a check mark.

1 Chapter 1: The PCAP-31-03 Exam Blueprint and Test-Day Logistics

1.1 The PCAP-31-03 Blueprint, Scoring, and Exam-Day Logistics

2 Chapter 2: Modules, Packages, and the Standard Library

2.1 Module Imports and Namespace Mechanics 2.2 Module Construction and Execution 2.3 Package Hierarchy and Navigation 2.4 Standard Library Modules and Pip

3 Chapter 3: Exceptions and Error Handling

3.1 Exception Control Flow and Syntax 3.2 The Built-in Exception Hierarchy 3.3 Raising Exceptions and Assertions 3.4 Custom Exceptions and Exception Chaining

4 Chapter 4: Strings, Encodings, and Text Processing

4.1 Character Encodings and String Immutability 4.2 String Indexing, Slicing, and Comparison 4.3 String Inspection and Classification Methods 4.4 String Transformation and Formatting Methods 4.5 Iterating, Sorting, and Comparing Strings Against Other Types

5 Chapter 5: Object-Oriented Programming Fundamentals

5.1 Classes, Instances, and Attributes 5.2 Encapsulation, Privacy, and Name Mangling 5.3 Dunder Methods and Object Lifecycle 5.4 Discovering Class Structure: __name__, __module__, __bases__, and Identity

6 Chapter 6: Advanced OOP, Inheritance, and Metaprogramming

6.1 Single Inheritance, Method Overriding, and Polymorphism 6.2 Multiple Inheritance and Method Resolution Order (MRO) 6.3 Class Methods, Static Methods, and Properties

7 Chapter 7: Functional Python, Comprehensions, and Closures

7.1 List, Dictionary, and Set Comprehensions 7.2 Lambda Expressions and Higher-Order Functions 7.3 Variable Scopes, the LEGB Rule, and Closures

8 Chapter 8: Iterators, Generators, and File I/O

8.1 Iterators and the Iteration Protocol 8.2 Generator Functions and Generator Expressions 8.3 File Input/Output and Context Managers 8.4 Predefined Streams, Handles vs. Streams, and errno Diagnostics

8.4 Predefined Streams, Handles vs. Streams, and errno Diagnostics

Key Takeaways Explain This Topic AI

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.

  • 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 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:

  • 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 ( 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 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: 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