OSError is how the operating system's refusals reach Python: the file system, the network stack, and process machinery report failures as an error number (errno) plus a message, and Python raises them as OSError or one of its more specific subclasses. Reading an OSError means reading its payload: the errno, the system's own description, and the filename or address it names are the diagnosis; the class name alone is just the envelope.
open("/mnt/data/results.txt", "w")
Traceback (most recent call last):
File "main.py", line 1, in <module>
OSError: [Errno 28] No space left on device:
'/mnt/data/results.txt'
# the errno is the error: 28 = the disk (or quota) is full,
# which no amount of path-checking would have found
df -h, and df -i for inodes, the forgotten twin: a filesystem can be out of inodes with bytes to spare) and clear the actual constraint rather than retrying the write.with blocks (or explicit close in finally) so handles are released deterministically; raising the process limit (ulimit -n) buys time but does not stop a leak.An OSError with errno 5 (input/output error) on storage, or intermittent 11 (resource temporarily unavailable) under load, points below the software: failing disks, flaky network mounts, and exhausted system resources. Correlate with system logs; the Python traceback is the smoke, not the fire.
Why catch OSError instead of the specific subclass? Catching OSError catches every OS failure including ones you did not foresee, which suits broad boundaries (a top-level “couldn't complete the operation” handler). For specific remedies, catch the subclass, or inspect the exception's .errno attribute and branch on it.
Where does the list of errno meanings live? In the errno module: errno.errorcode maps numbers to names like ENOSPC, and os.strerror(n) gives the description text. Printing e.errno and os.strerror(e.errno) in a handler produces the same words the traceback shows.