configparser.NoSectionError: No section (tested)

configparser.NoSectionError: No section: 'database' usually doesn't mean your file is missing a section. It means the file was never read, and read() didn't say so. We reproduced that and four other causes on Python 3.13.5.

1. The file wasn't read (most common)

mkdir -p proj/app && cd proj
printf '[database]\nhost = db.local\n' > app/settings.ini
cat > app/main.py <<'PY'
import configparser
cfg = configparser.ConfigParser()
print("read:", cfg.read("settings.ini"))
print(cfg.get("database", "host"))
PY
cd app && python3 main.py
cd .. && python3 app/main.py 2>&1 | grep -E "^(read:|configparser)"

Output:

read: ['settings.ini']
db.local
read: []
configparser.NoSectionError: No section: 'database'

Measured on Python 3.13.5: cfg.read("settings.ini") resolves the path against the current working directory, not against the script. Run from app/, it works. Run from the project root, read() silently returns [] (no FileNotFoundError), the parser stays empty, and the first get() raises NoSectionError. That's why this error often shows up only under cron, systemd, an IDE run button, or Docker, which all start from a different directory.

Fix: build the path from __file__ and check what read() returned

import configparser, pathlib
cfg = configparser.ConfigParser()
here = pathlib.Path(__file__).resolve().parent
path = here / "proj" / "app" / "settings.ini"
found = cfg.read(path)
if not found:
    raise SystemExit(f"config not found: {path}")
print("read:", [pathlib.Path(p).name for p in found], "->", cfg.get("database", "host"))

Output:

read: ['settings.ini'] -> db.local

Measured: read() returns the list of files it actually parsed. An empty list means nothing was loaded, so fail loudly there with the full path rather than later with a confusing section error. (If the file must exist, cfg.read_file(open(path)) raises FileNotFoundError itself.)

2. Section names are case-sensitive (keys aren't)

import configparser
cfg = configparser.ConfigParser()
cfg.read_string("[Database]\nhost = db.local\n")
print(cfg.sections())
for call in (lambda: cfg.get("database", "host"),
             lambda: cfg["database"]["host"],
             lambda: cfg.get("Database", "HOST")):
    try:
        print("ok:", call())
    except Exception as e:
        print(type(e).__name__ + ":", e)

Output:

['Database']
NoSectionError: No section: 'database'
KeyError: 'database'
ok: db.local

Measured: [Database] isn't database: get() raises NoSectionError and cfg["database"] raises a plain KeyError. Option names are lowercased by default, so "HOST" works. Print cfg.sections() to see the exact names that were parsed.

3. [DEFAULT] isn't a section

import configparser
cfg = configparser.ConfigParser()
cfg.read_string("[DEFAULT]\ntimeout = 5\n")
print("sections():", cfg.sections(), "has_section:", cfg.has_section("DEFAULT"))
print("get DEFAULT:", cfg.get("DEFAULT", "timeout"))
try:
    cfg.set("DEFAULT", "retries", "3"); print("set DEFAULT ok")
    cfg.add_section("DEFAULT")
except Exception as e:
    print(type(e).__name__ + ":", e)
try:
    cfg.set("server", "port", "80")
except Exception as e:
    print(type(e).__name__ + ":", e)

Output:

sections(): [] has_section: False
get DEFAULT: 5
set DEFAULT ok
ValueError: Invalid section name: 'DEFAULT'
NoSectionError: No section: 'server'

Measured: DEFAULT doesn't show up in sections() and has_section("DEFAULT") is False, but get("DEFAULT", ...) and set("DEFAULT", ...) work. add_section("DEFAULT") raises ValueError. Writing to a section that doesn't exist yet (set("server", ...)) raises NoSectionError: call add_section("server") first, or assign cfg["server"] = {"port": "80"}.

4. A BOM at the start of the file

import configparser
open("bom.ini", "w", encoding="utf-8-sig").write("[database]\nhost = db.local\n")
cfg = configparser.ConfigParser()
try:
    cfg.read("bom.ini", encoding="utf-8")
except Exception as e:
    print(type(e).__name__ + ":", str(e).splitlines()[-1])
cfg.read("bom.ini", encoding="utf-8-sig")
print("utf-8-sig:", cfg.sections())

Output:

MissingSectionHeaderError: '\ufeff[database]\n'
utf-8-sig: ['database']

Measured: a file saved as "UTF-8 with BOM" (Notepad and some Windows editors do this) read as utf-8 starts with an invisible \ufeff, so the first header isn't recognised and you get MissingSectionHeaderError. Read with encoding="utf-8-sig", which accepts files with or without the BOM.

5. Optional settings: fallback= instead of try/except

import configparser
cfg = configparser.ConfigParser()
cfg.read("missing.ini")
print(cfg.get("database", "host", fallback="localhost"))
print(cfg.getint("database", "port", fallback=5432))
print(cfg.has_option("database", "host"))

Output:

localhost
5432
False

Measured: with fallback=, get() and getint() return the default when the section or the option is missing, even when the file didn't load at all, so nothing raises. Use it for optional settings only; for required ones, keep the check from the fix above or a missing file will quietly turn into defaults.

Frequently asked questions

What causes configparser.NoSectionError: No section?

Usually the file was never read: read() resolves paths against the current working directory and silently returns [] if the file isn't there. We measured the same script working from app/ and raising NoSectionError from the project root.

Why does configparser read() not raise an error for a missing file?

By design it skips files it can't open and returns the list of files it did read. We measured it returning []; check that list, or use read_file(open(path)) to get FileNotFoundError.

Are configparser section names case-sensitive?

Yes. We measured [Database] failing for get('database', ...) with NoSectionError while option names like HOST matched because they are lowercased by default.

Why is DEFAULT not in configparser sections()?

DEFAULT is a special fallback section, not a real one. We measured sections() returning [] and has_section('DEFAULT') False, while get('DEFAULT', ...) still worked.

How do I fix MissingSectionHeaderError with \ufeff?

The file has a UTF-8 BOM. We measured encoding='utf-8-sig' fixing it.

How do I give configparser a default value?

Pass fallback=, e.g. cfg.get('database', 'host', fallback='localhost'). We measured it working even when no file was loaded.

Written with AI assistance. Measured on 2026-10-09 with Python 3.13.5: read() with a relative path from two working directories, a __file__-based path with a check on read()'s return value, section-name case, the DEFAULT section, a UTF-8 BOM file, and fallback=. Every block was run verbatim and its output pasted from that run. PlainHeaders is our extension; everything else here is built-in Python.