configparser read() Missing File No Error (tested)

A typo'd config path is one of the most common deploy-time bugs — and with configparser, it doesn't look like a bug at all. We measured exactly what ConfigParser.read() does when the file isn't there.

1. read() on a missing file returns an empty list, not an exception

import configparser

c = configparser.ConfigParser()
result = c.read("/tmp/definitely_does_not_exist_12345.ini")
print(result)
print(c.sections())
[]
[]

Measured: no FileNotFoundError, no warning — read() just returns an empty list of successfully-read filenames, and the ConfigParser object stays empty. Every subsequent get() call without a fallback= then raises a generic NoSectionError or NoOptionError far away from the real cause, which is a typo'd path, not a missing key.

2. This is by design — read() accepts a list and silently skips whichever don't exist

c2 = configparser.ConfigParser()
result = c2.read(["/tmp/nonexistent_a.ini", "/tmp/real_config.ini", "/tmp/nonexistent_b.ini"])
print(result)
print(c2.sections())
['/tmp/real_config.ini']
['s']

Measured: read() is built to let you probe several candidate locations (e.g. /etc/app.ini, ~/.app.ini, ./app.ini) and use whichever exist — its return value is the list of paths that were actually read. The silent-skip behavior is intentional for that use case; the footgun is that a single missing path behaves identically to that same by-design fallback, so a typo gives you no signal to distinguish "intentionally optional" from "broken."

3. Fix: check the return value, or use read_file() / open() for a required file

# Option 1: check what was actually read
c3 = configparser.ConfigParser()
found = c3.read("/tmp/definitely_does_not_exist_12345.ini")
if not found:
    raise FileNotFoundError("config file not found")

# Option 2: open the file yourself — this DOES raise
with open("/tmp/definitely_does_not_exist_12345.ini") as f:
    c4 = configparser.ConfigParser()
    c4.read_file(f)
FileNotFoundError: [Errno 2] No such file or directory: '/tmp/definitely_does_not_exist_12345.ini'

Measured: if the config file is required (not an optional override layer), either check that read()'s return list is non-empty, or swap it for read_file(open(path)) — opening the file yourself raises the normal, immediate FileNotFoundError that read() was built to swallow.

Frequently asked questions

Does configparser.read() raise an error if the file doesn't exist?

No — we measured it returning an empty list with no exception at all. The ConfigParser object is simply left empty, and the missing file produces no direct signal.

How do I know if configparser actually read my config file?

We confirmed read() returns a list of the paths it successfully read. Checking that this list is non-empty (or contains your expected path) is the only reliable way to detect a missing file.

Why does configparser.read() accept a list of filenames?

Per the docs and what we measured, it's designed to probe multiple candidate config locations and silently use whichever exist — useful for optional layered configs like /etc/app.ini plus a user override, but indistinguishable from a typo'd single path.

Is there a configparser method that does raise on a missing file?

Yes — we measured read_file() raising FileNotFoundError when given an already-open file object, since the file-opening step (which is where the error naturally occurs) happens outside configparser, via your own open() call.

What happens if I call get() on a config that silently failed to load?

We confirmed it raises NoSectionError or NoOptionError (unless you passed fallback=) — a generic missing-key error that gives no indication the real problem was a missing file, not a missing key.

Written with AI assistance. Measured on 2026-10-03 with Python 3.13.5's built-in configparser module: called ConfigParser.read() on a nonexistent path and on a list mixing missing and real paths, and compared against open() and read_file() on the same missing path. PlainHeaders is our extension; Python's configparser module is built in.