Skip to main content

Lib/configparser.py

cpython 3.14 @ ab2d84fe1023/Lib/configparser.py

configparser is a pure-Python module with no C accelerator. It reads .ini-style files organized into named sections, each containing key = value or key: value pairs.

The class hierarchy is:

  • RawConfigParser -- base parser with no interpolation; all values are returned as raw strings.
  • ConfigParser -- adds BasicInterpolation by default.
  • SafeConfigParser -- deprecated alias for ConfigParser (retained for backward compatibility since 3.2; removed in 3.12 from the official interface but the name still exists as an alias).

Interpolation is a separate strategy object passed to the parser constructor. BasicInterpolation expands %(name)s references within the same section (falling back to DEFAULT). ExtendedInterpolation uses ${section:key} syntax and can cross section boundaries.

Map

LinesSymbolRolegopy
1-100Module header, constants, exception hierarchyError, NoSectionError, DuplicateSectionError, DuplicateOptionError, NoOptionError, InterpolationError and its subclasses, ParsingError, MissingSectionHeaderError.(stdlib pending)
100-300Interpolation, BasicInterpolation, ExtendedInterpolationStrategy classes; before_get is called with the raw value and the section/option context and returns the interpolated string.(stdlib pending)
300-600RawConfigParser.__init__, read, read_file, read_string, read_dict, _readParser core; _read is the state machine that tokenizes lines into section headers, key-value pairs, multi-line continuations, and comments.(stdlib pending)
600-800RawConfigParser.get, set, add_section, remove_section, remove_option, has_section, has_option, options, items, sectionsAccessor and mutator methods; all key lookups normalize keys through optionxform (lowercased by default).(stdlib pending)
800-1000ConfigParser.get, getint, getfloat, getboolean, _get_conv, fallbackTyped getters and the fallback parameter; _get_conv calls get and applies a conversion function; fallback is returned when the key or section is absent.(stdlib pending)
1000-1100RawConfigParser.writeSerializes the in-memory configuration back to a file in INI format; preserves section order (using dict insertion order) but does not preserve comments.(stdlib pending)

Reading

_read state machine (lines 300 to 600)

cpython 3.14 @ ab2d84fe1023/Lib/configparser.py#L300-600

def _read(self, fp, fpname):
elements_added = set()
cursect = None # None, or a dict
sectname = None
optname = None
lineno = 0
indent_level = 0
e = None # None, or an exception

for lineno, line in enumerate(fp, start=1):
comment_start = sys.maxsize
# strip inline comments
inline_prefixes = {p: -1 for p in self._inline_comment_prefixes}
while comment_start == sys.maxsize and inline_prefixes:
next_prefixes = {}
for prefix, index in inline_prefixes.items():
index = line.find(prefix, index+1)
if index == -1:
continue
next_prefixes[prefix] = index
if comment_start > index:
comment_start = index
inline_prefixes = next_prefixes
for prefix in self._comment_prefixes:
if value.strip().startswith(prefix):
comment_start = 0
break
if comment_start == sys.maxsize:
comment_start = None

value = line[:comment_start].strip()
if not value:
if self._empty_lines_in_values:
if (cursect is not None and optname and
cursect[optname] is not None):
cursect[optname].append('')
else:
indent_level = sys.maxsize
continue

# is it a section header?
mo = self.SECTCRE.match(value)
if mo:
sectname = mo.group('header')
...
# no section header in the file?
elif cursect is None:
raise MissingSectionHeaderError(fpname, lineno, line)
# an option line?
else:
indent_level = len(line) - len(line.lstrip())
if (cursect is not None and optname and
indent_level > self._default_indent_level):
# continuation line
cursect[optname].append(value)
else:
mo = self._optcre.match(value)
if mo:
optname, vi, optval = mo.group('option', 'vi', 'value')
optname = self.optionxform(optname.rstrip())
cursect[optname] = [optval]

_read is a line-by-line state machine with four token types: blank lines, section headers (matched by SECTCRE), key-value lines (matched by _optcre), and continuation lines (detected by comparing the current line's indent level to _default_indent_level).

Multi-line values are accumulated as a list in cursect[optname]. After the loop, all lists are joined with '\n' to produce the final string. This means trailing newlines within a multi-line value are preserved. Leading whitespace on continuation lines is stripped by the indent-level check.

Inline comments are stripped before the line is classified. The scan for inline comment prefixes is incremental: it tracks the earliest prefix found and excludes prefixes that appear only inside quoted strings by advancing the search index on each pass through inline_prefixes.

BasicInterpolation.before_get (lines 100 to 300)

cpython 3.14 @ ab2d84fe1023/Lib/configparser.py#L100-300

class BasicInterpolation(Interpolation):
def before_get(self, parser, section, option, value, vars):
L = []
self._interpolate_some(parser, option, L, value, section, vars, 1)
return ''.join(L)

def _interpolate_some(self, parser, option, accum, rest, section, map, depth):
rawval = parser.get(section, option, raw=True, fallback=rest)
if depth > MAX_INTERPOLATION_DEPTH:
raise InterpolationDepthError(option, section, rawval)
while rest:
p = rest.find("%")
if p < 0:
accum.append(rest)
return
if p > 0:
accum.append(rest[:p])
rest = rest[p:]
# p is now 0
c = rest[1:2]
if c == "%":
accum.append("%")
rest = rest[2:]
elif c == "(":
m = self._KEYCRE.match(rest)
if m is None:
raise InterpolationSyntaxError(option, section,
"bad interpolation variable reference %r" % rest)
var = parser.optionxform(m.group(1))
rest = rest[m.end():]
try:
v = map[var]
except KeyError:
raise InterpolationMissingOptionError(
option, section, rawval, var) from None
if "%" in v:
self._interpolate_some(parser, option, accum, v,
section, map, depth + 1)
else:
accum.append(v)
else:
raise InterpolationSyntaxError(
option, section,
"'%%' must be followed by '%%' or '(', "
"not %r" % (c,))

_interpolate_some walks through the value string looking for %( sequences. When it finds one it extracts the key name, looks it up in map (which contains the section's options merged with vars and DEFAULT), and recurses if the replacement value itself contains % references. The depth counter prevents infinite recursion: after MAX_INTERPOLATION_DEPTH (10) levels, InterpolationDepthError is raised.

%% is the escape sequence for a literal %. Any other character after % is a syntax error.

ExtendedInterpolation uses a different regex (${section:key} or ${key}) and calls parser.get(section, key, raw=True) to resolve cross-section references.

ConfigParser.get with DEFAULT fallback (lines 800 to 1000)

cpython 3.14 @ ab2d84fe1023/Lib/configparser.py#L800-1000

def get(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
try:
d = self._unify_values(section, vars)
except NoSectionError:
if fallback is _UNSET:
raise
else:
return fallback
option = self.optionxform(option)
try:
value = d[option]
except KeyError:
if fallback is _UNSET:
raise NoOptionError(option, section)
else:
return fallback

if raw:
return value
else:
return self._interpolation.before_get(self, section, option, value, d)

def _unify_values(self, section, vars):
try:
d = self._defaults.copy()
except AttributeError:
d = {}
if section != self.default_section:
try:
d.update(self._sections[section])
except KeyError:
if vars is None:
raise NoSectionError(section) from None
if vars:
for key, value in vars.items():
d[self.optionxform(key)] = value
return d

_unify_values builds a single dict for a lookup by merging three sources in priority order: DEFAULT values first (lowest priority), section values on top, and vars (caller-supplied overrides) last. This merged dict is passed to before_get so that interpolation can resolve %(key)s references against all three sources without extra lookups.

optionxform normalizes keys to lowercase by default. Overriding it with str (i.e., identity) makes the parser case-sensitive.

The fallback=_UNSET sentinel pattern allows callers to distinguish "section or option not found, use my default" from "section or option not found, raise". The same sentinel is used in getint, getfloat, and getboolean via _get_conv.

gopy mirror

configparser is pending. The port is straightforward: _read is a line scanner with no recursion, BasicInterpolation is a string-walk loop, and the DEFAULT fallback is a dict merge. The main subtlety is preserving key order and DEFAULT propagation semantics across sections. write must reproduce the key = value format and multi-line continuation indent exactly.