INI Cheatsheet - Sections, Keys & Config File Syntax
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]\nhost = localhost\nport = 3306[server]\nlisten = 8080\nmax_connections = 100[logging]\nlevel = info\nfile = /var/log/app.log[user]\nname = Alice\nemail = alice@example.com[cache]\ndriver = redis\nttl = 3600PHP php.ini Example 9
[PHP]\nmemory_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]\nname = Your Name\nemail = you@example.com[core]\neditor = vim\nautocrlf = input[alias]\nco = checkout\nbr = branch\nlg = log --oneline --graph[remote "origin"]\nurl = https://github.com/user/repo.git[diff]\ntool = vimdiff[merge]\ntool = vimdiff\nconflictstyle = diff3[pull]\nrebase = trueWindows INI Example 5
path = C:\\Program Files\\App[Environment]\nPATH = %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 |
|---|---|---|
; 注释 | 分号注释,最通用 | ; 这是注释 |
# 注释 | 井号注释,部分解析器支持 | # 这也是注释 |
[section] | 节(section)定义,配置分组的唯一手段 | [database] |
key = value | 键值对,等号两边空格会被自动去除 | host = localhost |
key = "quoted value" | 带引号的值,含空格或特殊字符时使用 | name = "Alice B" |
key = | 空值 | path = |
boolean = true | 布尔值(true/false/yes/no/on/off) | display_errors = On |
array = a,b,c | 逗号分隔的数组(解析器相关) | tags = a,b,c |
hex = 0xFF | 十六进制值(部分解析器支持) | mask = 0xFF |
percent = 50% | 百分比值 | percent = 50% |
[core] editor / autocrlf | Git 核心配置:默认编辑器与换行转换 | [core]\neditor = vim |
[alias] co / br / lg | Git 命令别名 | lg = log --oneline --graph |
[merge] conflictstyle | 合并冲突显示风格(diff3) | [merge]\ntool = vimdiff\nconflictstyle = diff3 |
[PHP] memory_limit | PHP 内存限制 | memory_limit = 256M |
post_max_size / upload_max_filesize | POST 数据与上传文件大小上限 | post_max_size = 25M |
session.gc_maxlifetime | Session 最大生存时间(秒) | session.gc_maxlifetime = 1440 |
date.timezone | 时区设置 | date.timezone = Asia/Shanghai |
config.read("config.ini") | Python 读取 INI 文件 | config.read("config.ini") |
config.getint("section", "port") | 按类型读取(getint/getfloat/getboolean) | config.getint("server", "port") |
config["DEFAULT"]["key"] | 所有节继承的默认值 | config["DEFAULT"]["encoding"] |
Common pitfalls
Symptom配置文件的第一个节读不到,报 KeyError 或 section 不存在
Cause文件开头有 UTF-8 BOM(\xEF\xBB\xBF),BOM 被当成了节名的一部分。
Fix把文件另存为 UTF-8 无 BOM,或在读取前先剥离 BOM 字节。
Symptom值里出现多余的引号,比如程序读到的是 `"hello"` 而不是 `hello`
Cause不同解析器对引号的处理不一致:有的去掉外层引号,有的原样保留。
Fix值中不含空格和特殊字符时干脆不加引号;必须加时先确认目标解析器的实际行为。
Symptom配置项怎么改都不生效,程序像是完全没读这个文件
Cause注释符号用错了,该解析器只认分号不认井号(或相反),整行被当成注释。
Fix统一改用分号 `;` 作为注释符,这是跨解析器最通用的写法。
SymptomWindows 路径写进去后目录拼错,多出一层反斜杠或找不到目录
CauseINI 中的反斜杠是转义符,单个 `\` 会被当成转义起始。
Fix用双反斜杠 `C:\\Program Files\\App` 写出,或直接改用正斜杠。
Symptom想按层级组织配置,结果解析器直接报错
Cause标准 INI 不支持嵌套节,`[[nested]]` 只是部分工具的非标准扩展。
Fix改用 YAML 或 TOML 这类真正支持嵌套的格式;必须用 INI 时用下划线模拟层级,如 `db_host = ...`。
Symptom同一个键写了两次,最后读到哪一个不确定
Cause重复键的行为由解析器决定:有的后者覆盖前者,有的直接报错。
Fix保证每个节内键唯一;写配置的程序应先读后写而不是整段覆盖拼接。
Symptom读取 `%TEMP%` 这类写法时拿到的是字面量而不是展开后的路径
Cause变量插值是 Windows 系统配置文件的特性,多数程序自己的 INI 解析器并不支持。
Fix把路径由程序在运行时拼接传入配置文件,或改用 Python 的 ExtendedInterpolation 并改用 `$(...)` 语法。
Troubleshooting
1检查文件开头是否有 BOM
head -c 3 config.ini | xxd若输出为 efbb bf,说明存在 UTF-8 BOM,需要重新保存为无 BOM 编码。
2打印解析器实际读到的全部节与键
python -c "import configparser; c=configparser.ConfigParser(); c.read('config.ini'); print([s for s in c.sections()]); print(dict(c['DEFAULT']))"sections() 为空说明文件根本没被读进来,DEFAULT 里的内容则是所有节的公共默认值。
3按类型读取单个值以确认解析结果
python -c "import configparser; c=configparser.ConfigParser(); c.read('config.ini'); print(c.getint('server', 'port'))"能打印出整数说明解析成功;若报 ValueError,说明值不是合法数字或带了残留引号。
4查看原始行以核对注释符与引号
grep -n '^\s*['\";#[]' config.ini输出可逐行核对节定义、注释行和取值写法,异常写法一眼可见。
5确认 Git 实际生效的配置
git config --list --show-origin输出会标注每个配置项来自哪个文件,用来定位全局与仓库级配置相互覆盖的问题。
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