INI Cheatsheet - Sections, Keys & Parser Quirks
INI is simple, but parsers differ: the comment character, quote handling, nesting support and variable interpolation all vary by implementation. This reference organizes 8 groups of basic syntax and data types, plus four real-world scenarios — Windows, php.ini, Git config and Python configparser — and a dedicated parsing-caveats group on cross-parser differences. After reading you can judge quickly whether a given INI block is safe in the target parser and avoid common traps like BOM, duplicate keys and comment-character issues.
Typical Use Case
For developers who read or maintain older configuration files: Windows application settings, .gitconfig, php.ini, and custom formats consumed by Python configparser. The syntax is trivial, a section header plus key and value pairs, but parsers differ in the details that break cross-tool workflows: whether the comment marker is a semicolon or a hash, whether quotes survive parsing, how duplicate keys are handled, how to escape backslashes in Windows paths, and the UTF-8 BOM that breaks the first section. The sheet shows real PHP and Git fragments alongside the Windows specifics, plus the configparser read, write and typed-access calls.
Basic Syntax 7
; this is a comment# this is also a comment[section]key = valuekey = "quoted value"key =; key = commented outData Types 7
string = hellointeger = 42float = 3.14boolean = truearray = value1,value2,value3hex = 0xFFpercent = 50%Common Applications 5
[database]
host = localhost
port = 3306[server]
listen = 8080
max_connections = 100[logging]
level = info
file = /var/log/app.log[user]
name = Alice
email = alice@example.com[cache]
driver = redis
ttl = 3600PHP php.ini Example 9
[PHP]
memory_limit = 256Mupload_max_filesize = 20Mpost_max_size = 25Mmax_execution_time = 30display_errors = Onerror_reporting = E_ALLdate.timezone = Asia/Shanghaisession.save_path = /tmpsession.gc_maxlifetime = 1440Git Config Example 7
[user]
name = Your Name
email = you@example.com[core]
editor = vim
autocrlf = input[alias]
co = checkout
br = branch
lg = log --oneline --graph[remote "origin"]
url = https://github.com/user/repo.git[diff]
tool = vimdiff[merge]
tool = vimdiff
conflictstyle = diff3[pull]
rebase = trueWindows INI Example 5
path = C:\\Program Files\\App[Environment]
PATH = %PATH%;C:\\bintemp = %TEMP%\\appapp_dir = ${APPDIR}key = value ; trailing commentPython configparser 6
config.read("config.ini")config.write(open("config.ini", "w"))config["section"]["key"]config["section"]["key"] = "value"config["DEFAULT"]["key"]config.getint("section", "port")Parsing Caveats 6
; vs #key="value" vs key = value[[nested]]BOM: \xEF\xBB\xBFkey = ; not a commentduplicate keysParameter matrix
| Parameter | Effect | Example |
|---|---|---|
; comment | Semicolon comment, the most portable form | ; this is a comment |
# comment | Hash comment, understood only by some parsers | # this is a comment too |
[section] | Section header, which is the only way to group configuration | [database] |
key = value | Key/value pair; spaces around the equals sign are stripped automatically | host = localhost |
key = "quoted value" | A quoted value, needed when the text contains spaces or special characters | name = "Alice B" |
key = | A key carrying no value at all | path = |
boolean = true | A boolean, written true/false, yes/no or on/off | display_errors = On |
array = a,b,c | A comma-separated array, depending on the parser | tags = a,b,c |
hex = 0xFF | A hexadecimal value, supported by some parsers | mask = 0xFF |
percent = 50% | A percentage value | percent = 50% |
[core] editor / autocrlf | Git core settings: the default editor and line-ending conversion | [core]
editor = vim |
[alias] co / br / lg | Git command aliases | lg = log --oneline --graph |
[merge] conflictstyle | How merge conflicts are displayed, the diff3 style | [merge]
tool = vimdiff
conflictstyle = diff3 |
[PHP] memory_limit | The PHP memory limit | memory_limit = 256M |
post_max_size / upload_max_filesize | Caps on the POST body and upload size | post_max_size = 25M |
session.gc_maxlifetime | Maximum session lifetime, in seconds | session.gc_maxlifetime = 1440 |
date.timezone | The timezone setting | date.timezone = Asia/Shanghai |
config.read("config.ini") | Reading an INI file from Python | config.read("config.ini") |
config.getint("section", "port") | Typed reads through getint, getfloat and getboolean | config.getint("server", "port") |
config["DEFAULT"]["key"] | Defaults inherited by every section | config["DEFAULT"]["encoding"] |
Common pitfalls
SymptomThe first section cannot be read and the parser reports a missing section or a KeyError.
CauseThe file starts with a UTF-8 BOM (\xEF\xBB\xBF), and the BOM got absorbed into the section name.
FixRe-save as UTF-8 without a BOM, or strip the BOM bytes before parsing.
SymptomThe value keeps its quotes, so the program reads "hello" instead of hello.
CauseParsers disagree about quotes: some strip the outer pair, others keep it verbatim.
FixSkip the quotes entirely when the value holds no spaces or special characters; when you do need them, confirm the behaviour of the parser you actually target.
SymptomThe setting never takes effect, as if the file were never read at all.
CauseThe wrong comment marker was used and this parser only accepts the other one, so the whole line became a comment.
FixStandardise on the semicolon ; as the comment character, which is the most portable choice across parsers.
SymptomA Windows path gains an extra level of backslashes or the directory is not found.
CauseIn INI the backslash is an escape character, so a single one starts an escape sequence.
FixWrite doubled backslashes such as C:\\Program Files\\App, or switch to forward slashes.
SymptomAttempting to nest configuration makes the parser fail outright.
CauseStandard INI has no nested sections; the double-bracket form is a non-standard extension of a few tools.
FixMove to YAML or TOML, which really do nest; if INI is mandatory, fake the hierarchy with underscore-joined key names such as db_host = ....
SymptomThe same key appears twice and it is unclear which value wins.
CauseDuplicate-key behaviour is parser-defined: some let the later entry win, others raise an error.
FixKeep every key unique within a section, and have the writing side read-modify-write instead of rewriting the whole file.
SymptomA value like %TEMP% comes back as literal text instead of an expanded path.
CauseVariable interpolation is a Windows system-config feature that most application INI parsers simply do not implement.
FixHave the program build the path at runtime and pass it into the config, or switch to Python ExtendedInterpolation and write it in the $(...) form.
Troubleshooting
1Check whether the file starts with a BOM
head -c 3 config.ini | xxdA leading efbb bf means there is a UTF-8 BOM and the file needs re-saving without one.
2Print every section and key the parser actually sees
python -c "import configparser; c=configparser.ConfigParser(); c.read('config.ini'); print([s for s in c.sections()]); print(dict(c['DEFAULT']))"An empty sections() means the file was never loaded; entries under DEFAULT are the shared fallbacks for all sections.
3Read a single value by type to confirm the parse
python -c "import configparser; c=configparser.ConfigParser(); c.read('config.ini'); print(c.getint('server', 'port'))"Printing an integer means parsing worked; a ValueError means the value is not a valid number or still carries quotes.
4Dump the raw lines to check comment markers and quoting
grep -n '^\s*['\";#[]' config.iniThe output lets you compare section headers, comment lines and value syntax one by one, and anything odd stands out immediately.
5Confirm which Git configuration is actually in effect
git config --list --show-originThe output annotates where each setting came from, which pinpoints global versus repository-level overrides.
Command Examples
Read by section and by type instead of hand-splitting
import configparser
config = configparser.ConfigParser()
config.read("config.ini")
config.getint("database", "port")config.read returns the list of files it actually loaded and silently returns an empty list when the file is missing, so you must check it explicitly. Use getint / getfloat / getboolean so the parser converts and validates the type instead of you splitting on "=" yourself.
Output
3306
The DEFAULT section is inherited by every other section
[DEFAULT]
mode = dev
[server]
host = 127.0.0.1DEFAULT is not a regular section: has_section does not match it and it never shows up in items(), yet it is visible from every section. It is the natural place for shared defaults, leaving each section to declare only its differences.
Output
config.get('server', 'mode') -> 'dev'Writing the whole file back drops every comment
config["cache"]["ttl"] = "7200"
with open("config.ini", "w") as f:
config.write(f)ConfigParser keeps only parsed data, never the original text, so trailing comments and formatting vanish on write. If the file is comment-heavy for humans, use a comment-preserving format or patch the single line instead of rewriting the whole file.
It looks right, but the parser sees an empty value
key = ; not a commentTrailing comments are only honoured by some parsers; most treat the semicolon and everything after it as part of the value. Quote the value if it must contain a semicolon, or avoid the character entirely.
Output
config.get('section', 'key') -> '; not a comment'Windows paths need their backslashes escaped
path = C:\\Program Files\\AppThe escape character in ini is the backslash itself. Without doubling, \P and \A are consumed as escape sequences and part of the path silently disappears — no error is raised. Prefer forward slashes in cross-platform config files.
Common Pitfalls
- A wrong section or key name does not warn you — you simply get no value. Most getters raise KeyError/NoSectionError on a missing key, but any name that collides with DEFAULT returns silently, which is an easy source of misdiagnosis.
- config.read() returns an empty list for a missing file instead of raising. Forgetting the file means everything later falls back to DEFAULT values and the config looks live when it never loaded.
- Duplicate keys are parser-defined: the later one silently wins, or the parser errors. Repeating a key in one section is a notoriously hard bug to spot.
- getint raises ValueError on a non-numeric value, so something like 3306ms in the file explodes at read time.
- Section and key names are lowercased by default (unless optionxform is overridden), so a [Server] section read as config.get("Server", ...) misses on some implementations.
- Writing the whole file back discards every comment and all original formatting — do not rewrite a hand-maintained config through a parser.
Tips
- INI syntax is simple but has no nesting; use YAML or TOML for complex configs.
- Comment support varies by parser; semicolon (;) is the most portable.
- Spaces around the = are optional; parsers strip them automatically.
- Escape backslashes in Windows paths; prefer forward slashes or double backslashes.
- Python configparser has no default var interpolation; use ExtendedInterpolation.
- Use UTF-8 without BOM for INI files to avoid first-section parse failures.
FAQ
Why does my INI parser ignore the first section?
In most cases the file was saved with a UTF-8 BOM — the three bytes \xEF\xBB\xBF at the start — which the parser folds into the first section name and then fails to match. Save as UTF-8 without BOM. Next suspect is the comment character: semicolon ; is universal while hash # is only honoured by some parsers, so prefer semicolons. Once encoding is clean, look for duplicate keys — the same key twice in one section behaves unpredictably, with some parsers letting the later value win and others erroring out.
INI has no nested sections — what do I use when the config needs layers?
Standard INI only offers two levels, a section plus key-value pairs, and writing [[nested]] is a non-standard extension that other tools may reject. When you need a list inside a section, encode it as a comma-separated value such as array = value1,value2,value3. If the structure genuinely needs nested maps and lists of maps, move the file to YAML or TOML, which support arbitrary nesting; INI simply cannot express it. Keep genuinely flat configurations in INI so the migration stays cheap.
Why do Windows paths with backslashes keep breaking in INI files?
The backslash is doing double duty as both a path separator and an escape character. path = C:\\Program Files\\App is the way to tell the parser that each \ means a single literal backslash, whereas a lone C:\Program Files may be read as the start of an escape sequence and mangle the second segment. The simplest fix is to use forward slashes, which most parsers accept and Windows resolves fine. When the value contains spaces, quote the whole thing: app_dir = "C:\\Program Files\\App".
How do I handle types and defaults when reading INI files with configparser?
Load with config.read("config.ini"), read values through config["section"]["key"], and write with config["section"]["key"] = "value" followed by config.write(open("config.ini", "w")). Typed accessors save you manual casting: config.getint("section", "port") returns 3306 as an int, and getfloat and getboolean work the same way. Defaults live in config["DEFAULT"]["key"] and are inherited by every section. One catch: configparser does not interpolate variables by default, so %VAR% references only work once you explicitly enable ExtendedInterpolation.
When is INI actually the right choice for a config format?
INI fits flat, single-level, key-driven configuration: database connections such as [database] host = localhost port = 3306, server limits, log levels, cache driver and TTL, user details. The php.ini excerpt in the cheatsheet follows the same shape, with one switch per line like memory_limit = 256M or date.timezone = Asia/Shanghai. The decision rule is simple: the moment you want to express a list of objects or deep nesting, INI is the wrong tool and YAML or TOML should take over.
Official References
Each command links to its official documentation below, so you can verify the latest usage and read deeper.
Maintained by LaoHand
Publicly updated on Jul 21, 2026, continuously proofread against official docs.
Contact Us
Wrong command or description? Send us corrections, business inquiries or product feedback by email.
Contact Us