skip to content

How do OSError subclasses such as FileNotFoundError relate to errno?

level: middleimportance: should knowfreq 48%

answer

  1. One flat class became a family
  2. The change landed in Python 3.3
  3. The numeric code still rides along
  4. except FileNotFoundError, not an errno test
  5. err.errno, err.strerror, err.filename

basics

~10 s

Python 3.3 gave the common operating-system error codes their own OSError subclasses, so you write except FileNotFoundError instead of inspecting err.errno. The errno attribute is still populated for codes without a dedicated class.

solid answer

~40 s

Before Python 3.3 every failed system call raised one flat `OSError`, and you had to catch it and branch on `err.errno` against constants from the `errno` module. PEP 3151 in 3.3 mapped the common codes onto real subclasses: `FileNotFoundError`, `FileExistsError`, `PermissionError`, `IsADirectoryError`, `NotADirectoryError`, `InterruptedError`, `TimeoutError`, `BlockingIOError`, `ProcessLookupError`, and the `ConnectionError` family with `ConnectionResetError`, `ConnectionRefusedError`, `ConnectionAbortedError` and `BrokenPipeError`. Matching is by class, so `except OSError` still catches all of them, and you narrow only where you need to. The instance still carries `errno`, `strerror` and `filename`, which is how you handle codes that never got their own class. The same reorganisation made `IOError` and `EnvironmentError` plain aliases of `OSError`.

code

python · 6 lines
python
import errno

try:
    open("/no/such/file")
except FileNotFoundError as err:
    print(type(err).__name__, isinstance(err, OSError), err.errno == errno.ENOENT, err.filename)

go deeper

for a junior

Know that a missing file raises FileNotFoundError and a denied one raises PermissionError, and that both are OSError underneath. Writing the narrow handler instead of inspecting an error number is the habit to show.

for a middle

Explain that Python 3.3 mapped errno values onto subclasses, that the base class still catches all of them, and that errno, strerror and filename remain on the instance for codes with no dedicated class.

for a senior

Show judgement about where to catch: the family at a boundary where any failure means the same thing, the narrow class where recovery differs. Be ready to discuss platform variation in which subclass a torn-down connection produces.

for a principal

Own how operating-system failures are classified as they cross your service boundaries — which are retryable, which are fatal, how they map onto your public error contract — so callers never have to inspect an error number to decide what to do.

## What the tree looked like before Operating-system failures arrive in Python as `OSError`. Historically that was all the information the class gave you: a single flat type covering a missing file, a permission denial, a broken pipe and a refused connection alike. To distinguish them you caught the base class and inspected the numeric `errno` attribute against constants from the `errno` module: ```python import errno try: open("/no/such/file") except OSError as err: if err.errno != errno.ENOENT: raise ``` That pattern worked but was noisy, easy to get wrong, and inverted the normal shape of Python error handling: you caught broadly and then re-raised what you did not want, instead of naming what you did. ## PEP 3151 and the subclasses Python 3.3 reorganised this. The common `errno` values were given dedicated `OSError` subclasses, and the interpreter selects the right class when it constructs the exception. The ones worth knowing by name: * `FileNotFoundError`, `FileExistsError` * `PermissionError` * `IsADirectoryError`, `NotADirectoryError` * `NotImplementedError` is *not* one of these — it is a `RuntimeError` subclass, a common trip-up * `InterruptedError`, `BlockingIOError`, `ChildProcessError`, `ProcessLookupError` * `TimeoutError` * `ConnectionError`, with `ConnectionResetError`, `ConnectionRefusedError`, `ConnectionAbortedError` and `BrokenPipeError` beneath it Because exception matching is by class, this is purely additive for callers. `except OSError` still catches every one of them — useful at a boundary where any filesystem failure means the same thing. `except FileNotFoundError` narrows to exactly the case you can recover from. And `except ConnectionError` catches the whole family of socket teardown failures without listing four classes. The same PEP collapsed the old aliases: `IOError`, `EnvironmentError` and `WindowsError` are the same object as `OSError`, not separate classes. Code that catches `IOError` is catching `OSError` under another spelling, which is why modern code simply writes `OSError`. ## errno did not go away The subclasses cover the common codes, not all of them. The instance still carries the raw data: * `errno` — the numeric code, comparable against `errno.ENOENT`, `errno.EACCES` and friends * `strerror` — the operating system's message for that code * `filename` and `filename2` — the paths involved, when the failing call had them So the modern pattern is layered: name the subclass when one exists, and fall back to catching `OSError` and testing `errno` for codes such as `ENOSPC` (no space left on device) or `EXDEV` (cross-device link) that have no dedicated class. Note that `errno` is `None` on `OSError` instances constructed by hand or raised by code that did not come from a real system call, so a comparison should tolerate that. ```python import errno try: open("/no/such/file") except FileNotFoundError as err: print(isinstance(err, OSError), err.errno == errno.ENOENT, err.filename) ``` ## Portability caveats The mapping from an operating-system condition to a Python class runs through `errno`, and the platforms do not agree perfectly. Opening a directory as a file raises `IsADirectoryError` on Linux but succeeds or fails differently elsewhere; a connection torn down mid-transfer may surface as `ConnectionResetError` on one platform and `BrokenPipeError` on another. The defensive habit is to catch the family (`ConnectionError`, or `OSError` itself) at boundaries where you only need to know that the operation failed, and to reserve the narrow classes for cases where recovery genuinely differs — for example, treating a missing file as "create it" and a permission denial as "stop and report". ## TimeoutError is shared `TimeoutError` is a built-in `OSError` subclass, originally for the `ETIMEDOUT` system error. Python 3.11 made the asynchronous timeout error the same class: `asyncio.TimeoutError` is now an alias of the built-in, and `socket.timeout` had already become one in 3.10. That means a single `except TimeoutError` covers a blocking socket timeout and an awaited operation that timed out, which is a small but frequently-missed simplification when modernising older code. ## What an interviewer is checking That you write `except FileNotFoundError` rather than an `errno` comparison; that you know the base class still catches everything, so narrowing is a choice and not a risk; that you can reach for `errno` when the code has no dedicated subclass; and that you do not confuse `NotImplementedError` — which is a `RuntimeError` — with anything in this family. ## Reading a code you do not recognise The `errno` module also carries `errorcode`, a mapping from the numeric value back to its symbolic name, so `errno.errorcode[err.errno]` turns an unfamiliar number in a log into something searchable, and `os.strerror` renders the platform's message for a code you already hold. In practice, logging the exception's class name alongside `errno` and `filename` is enough to reconstruct a filesystem failure after the fact without reproducing it — which matters, because these are exactly the failures that depend on the state of a machine you no longer have in front of you.

  • How would you handle an errno value that has no dedicated subclass, such as running out of disk space?
    Catch `OSError` and compare `err.errno` against the constant from the `errno` module — `errno.ENOSPC` in that case — re-raising anything that does not match. That is the pre-3.3 pattern, still correct for the codes PEP 3151 did not promote. Guard for `errno` being `None`, which happens on instances not produced by a real system call.
  • Does catching OSError still catch FileNotFoundError?
    Yes. Exception matching in Python is by class, and every one of these subclasses derives from `OSError`, so a handler naming the base catches the whole family. That is why the 3.3 reorganisation broke nothing: narrowing became possible without any existing handler changing behaviour. Catch the base where any failure means the same thing, and narrow where recovery genuinely differs.
  • Which of these classes is not an OSError subclass despite sounding like one?
    `NotImplementedError` derives from `RuntimeError`, not `OSError` — it signals an abstract method or unfinished code path, nothing to do with the operating system. It is also distinct from the `NotImplemented` singleton, which a binary dunder method returns to tell Python to try the reflected operation instead.

saying these in an interview costs you the question

  • Still branches on err.errno when a subclass exists
  • Thinks IOError is a different class from OSError
  • Believes the subclasses stopped populating errno
  • Assumes catching OSError misses FileNotFoundError
  • Calls NotImplementedError an OSError subclass
  • Expects identical subclasses across every platform

context