configparser Percent Sign InterpolationSyntaxError (tested)
A config file value like discount = 50% off looks completely ordinary. We measured Python's default configparser.ConfigParser() refusing to read it at all, raising an exception on a plain percent sign that has nothing to do with string formatting.
1. A literal % in a value raises an exception on read
import configparser
c = configparser.ConfigParser()
c.read_string("""
[section]
discount = 50% off
""")
c["section"]["discount"]
Traceback (most recent call last):
...
configparser.InterpolationSyntaxError: '%' must be followed by '%' or '(', found: '% off'
Measured: the file parses fine — read_string() itself raises no error. The exception only happens when the value is actually accessed, and it comes from ConfigParser's default interpolation feature, not from anything wrong with the INI syntax. A % not immediately followed by another % or a ( is invalid under that feature, because plain % is reserved for %(other_key)s-style substitutions.
2. Why: ConfigParser defaults to %-style interpolation, like old-style string formatting
Per the configparser docs, ConfigParser() defaults to BasicInterpolation, which lets one value reference another via %(key)s syntax (similar to old %-style string formatting). That feature reserves every % character, so any value containing a real, literal percent sign — a discount rate, a CPU usage threshold, a URL-encoded %20 — needs to escape it, or interpolation has to be turned off entirely.
3. Fix 1: escape the percent sign as %%
c.read_string("""
[section]
discount = 50%% off
""")
c["section"]["discount"]
'50% off'
Measured: doubling the % to %% in the source file produces a single literal % once read. This works, but means the file on disk doesn't show the value a human would actually type or expect — anyone hand-editing the config needs to know this rule.
4. Fix 2: disable interpolation entirely if you don't use it
c2 = configparser.ConfigParser(interpolation=None)
c2.read_string("""
[section]
discount = 50% off
""")
c2["section"]["discount"]
'50% off'
c3 = configparser.RawConfigParser() # also defaults to no interpolation
c3.read_string("""
[section]
discount = 50% off
""")
c3["section"]["discount"]
'50% off'
Measured: both ConfigParser(interpolation=None) and RawConfigParser() read the unescaped, literal 50% off correctly with no exception. Per the docs, RawConfigParser exists specifically for configs that don't use value interpolation. If your config doesn't reference other keys via %(key)s, either option avoids this entirely — only reach for %%-escaping if you're actively relying on interpolation elsewhere in the same file.
Frequently asked questions
Why does configparser raise an error on a value with a percent sign?
We measured it happening because ConfigParser's default BasicInterpolation reserves the % character for %(key)s-style value substitution — any % not followed by % or ( raises InterpolationSyntaxError when the value is read, even though the file parses without error.
Does the error happen when reading the config file or when accessing the value?
We confirmed it happens on access, not on read_string()/read() — the file loads successfully, and the exception only appears the moment you index into the section to get the value.
How do I put a literal percent sign in a configparser value?
We verified doubling it to %% in the source file (e.g. '50%% off') produces a single literal % once read back through the default ConfigParser.
How do I avoid escaping percent signs in configparser values entirely?
We confirmed that both configparser.ConfigParser(interpolation=None) and configparser.RawConfigParser() read a literal, unescaped % correctly with no exception — use either if your config doesn't rely on %(key)s-style interpolation.
Is this specific to the % character, or do other characters cause similar errors?
Based on the docs and what we measured, this is specific to % because it's the interpolation marker for BasicInterpolation; ordinary characters like $ or # don't trigger any equivalent parsing behavior in configparser values.
Written with AI assistance. Measured on 2026-10-03 with Python 3.13.5's built-in configparser module: read a config value containing a literal '%' with the default ConfigParser, then with RawConfigParser and with interpolation=None. PlainHeaders is our extension; Python's configparser module is built in.