Python configparser Case Sensitive Keys: optionxform (tested)

Python's ConfigParser lowercases every option name it reads, writes them back lowercased, and treats Port and port as the same key -- while section names stay case-sensitive. We measured each behavior and the fix: cfg.optionxform = str, which only works if you set it before calling read().

1. What ConfigParser does to case by default

import configparser, io

ini = """[Server]
HostName = example.com
Port = 8080
port = 9090
"""

cfg = configparser.ConfigParser(strict=False)
cfg.read_string(ini)
print("sections:", cfg.sections())
print("keys:", list(cfg["Server"]))
print("cfg['Server']['HOSTNAME'] ->", cfg["Server"]["HOSTNAME"])
print("Port ->", cfg["Server"]["Port"])
print("'server' in cfg ->", "server" in cfg)
out = io.StringIO(); cfg.write(out); print(out.getvalue().strip())
try:
    configparser.ConfigParser().read_string(ini)
except configparser.Error as e:
    print(type(e).__name__ + ":", e)
sections: ['Server']
keys: ['hostname', 'port']
cfg['Server']['HOSTNAME'] -> example.com
Port -> 9090
'server' in cfg -> False
[Server]
hostname = example.com
port = 9090
DuplicateOptionError: While reading from '<string>' [line  4]: option 'port' in section 'Server' already exists

Measured:

2. The fix: optionxform = str, set before reading

import configparser, io

ini = """[Server]
HostName = example.com
Port = 8080
"""

# Too late: optionxform set after reading
late = configparser.ConfigParser()
late.read_string(ini)
late.optionxform = str
print("set after read:", list(late["Server"]))

# Correct: set before reading
cfg = configparser.ConfigParser()
cfg.optionxform = str
cfg.read_string(ini)
print("set before read:", list(cfg["Server"]))
out = io.StringIO(); cfg.write(out); print(out.getvalue().strip())
try:
    cfg["Server"]["hostname"]
except KeyError as e:
    print("lookup is now case-sensitive: KeyError", e)

# Section names were always case-sensitive, and so is DEFAULT
d = configparser.ConfigParser()
d.read_string("[default]\ntimeout = 5\n[app]\n")
print("[default] is a normal section:", d.sections(), "| app timeout:", d["app"].get("timeout"))
set after read: ['hostname', 'port']
set before read: ['HostName', 'Port']
[Server]
HostName = example.com
Port = 8080
lookup is now case-sensitive: KeyError 'hostname'
[default] is a normal section: ['default', 'app'] | app timeout: None

Measured: optionxform is applied while parsing, so setting it after read() changes nothing for keys already loaded -- they stay lowercase. Set it right after constructing the parser. Afterwards write() keeps the original spelling, and lookups become case-sensitive (['hostname'] raises KeyError), so use the exact key name. The special DEFAULT section is matched case-sensitively too: [default] is just an ordinary section and its values are not inherited.

The same applies to RawConfigParser: the lowercasing comes from the default optionxform(), which returns optionstr.lower().

Related: configparser % sign InterpolationSyntaxError and configparser.read() ignoring a missing file.

Frequently asked questions

How do I make configparser keys case-sensitive?

Set cfg.optionxform = str right after creating the parser and before any read(), read_string() or read_file() call. We measured that setting it after reading leaves the already-loaded keys lowercase.

Are section names case-sensitive?

Yes, by default. [Server] and [server] are different sections, and 'server' in cfg is False for [Server]. Only option names are lowercased.

Why did my keys become lowercase in the saved file?

The default optionxform lowercases names on read, and write() saves the stored names. Any read-modify-write round trip with a default ConfigParser rewrites every key in lowercase.

What happens with two keys that differ only by case?

With the default strict=True: DuplicateOptionError. With strict=False the last value wins and the other is silently dropped. With optionxform = str they are kept as two separate options.

Is [default] the same as [DEFAULT]?

No. We measured that [default] is parsed as an ordinary section and its values are not inherited by other sections; only the exact name DEFAULT (configurable via default_section) is special.

Written with AI assistance. Measured on 2026-10-09 with Python 3.13.5's built-in configparser.ConfigParser, reading INI text with mixed-case keys and sections via read_string() and writing it back with write(). PlainHeaders is our extension; Python's configparser is built in.