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.

Config Formats·52 commands·Last updated 2026-07-21
iniConfig FilesKey-Value PairsSyntax

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
Semicolon comment (most common)
# this is also a comment
Hash comment (some parsers)
[section]
Section definition
key = value
Key-value pair (spaces around = optional)
key = "quoted value"
Quoted value
key =
Empty value
; key = commented out
Comment out a key-value pair

Data Types 7

string = hello
String
integer = 42
Integer
float = 3.14
Float
boolean = true
Boolean (true/false/yes/no/on/off)
array = value1,value2,value3
Array (comma-separated)
hex = 0xFF
Hex (some parsers)
percent = 50%
Percent value

Common Applications 5

[database] host = localhost port = 3306
Database config
[server] listen = 8080 max_connections = 100
Server config
[logging] level = info file = /var/log/app.log
Logging config
[user] name = Alice email = alice@example.com
User config
[cache] driver = redis ttl = 3600
Cache config

PHP php.ini Example 9

[PHP] memory_limit = 256M
Memory limit
upload_max_filesize = 20M
Upload file size
post_max_size = 25M
POST data size
max_execution_time = 30
Max execution time (seconds)
display_errors = On
Display errors
error_reporting = E_ALL
Error reporting level
date.timezone = Asia/Shanghai
Timezone setting
session.save_path = /tmp
Session storage path
session.gc_maxlifetime = 1440
Session max lifetime (seconds)

Git Config Example 7

[user] name = Your Name email = you@example.com
User info
[core] editor = vim autocrlf = input
Core config
[alias] co = checkout br = branch lg = log --oneline --graph
Command aliases
[remote "origin"] url = https://github.com/user/repo.git
Remote repository
[diff] tool = vimdiff
Diff tool config
[merge] tool = vimdiff conflictstyle = diff3
Merge tool config
[pull] rebase = true
Use rebase on pull

Windows INI Example 5

path = C:\\Program Files\\App
Windows path (escape backslashes)
[Environment] PATH = %PATH%;C:\\bin
Environment variable (reference system var)
temp = %TEMP%\\app
Compose path with system env var
app_dir = ${APPDIR}
Variable reference syntax (some parsers)
key = value ; trailing comment
Trailing comment (some parsers)

Python configparser 6

config.read("config.ini")
Read an INI file
config.write(open("config.ini", "w"))
Write an INI file
config["section"]["key"]
Read a key's value
config["section"]["key"] = "value"
Set a key-value pair
config["DEFAULT"]["key"]
Default value (inherited by all sections)
config.getint("section", "port")
Typed read (getint/getfloat/getboolean)

Parsing Caveats 6

; vs #
; vs #
key="value" vs key = value
key="value" vs key = value
[[nested]]
[[nested]]
BOM: \xEF\xBB\xBF
BOM: \xEF\xBB\xBF
key = ; not a comment
key = ; not a comment
duplicate keys
duplicate keys

Parameter matrix

ParameterEffectExample
; commentSemicolon comment, the most portable form; this is a comment
# commentHash comment, understood only by some parsers# this is a comment too
[section]Section header, which is the only way to group configuration[database]
key = valueKey/value pair; spaces around the equals sign are stripped automaticallyhost = localhost
key = "quoted value"A quoted value, needed when the text contains spaces or special charactersname = "Alice B"
key =A key carrying no value at allpath =
boolean = trueA boolean, written true/false, yes/no or on/offdisplay_errors = On
array = a,b,cA comma-separated array, depending on the parsertags = a,b,c
hex = 0xFFA hexadecimal value, supported by some parsersmask = 0xFF
percent = 50%A percentage valuepercent = 50%
[core] editor / autocrlfGit core settings: the default editor and line-ending conversion[core] editor = vim
[alias] co / br / lgGit command aliaseslg = log --oneline --graph
[merge] conflictstyleHow merge conflicts are displayed, the diff3 style[merge] tool = vimdiff conflictstyle = diff3
[PHP] memory_limitThe PHP memory limitmemory_limit = 256M
post_max_size / upload_max_filesizeCaps on the POST body and upload sizepost_max_size = 25M
session.gc_maxlifetimeMaximum session lifetime, in secondssession.gc_maxlifetime = 1440
date.timezoneThe timezone settingdate.timezone = Asia/Shanghai
config.read("config.ini")Reading an INI file from Pythonconfig.read("config.ini")
config.getint("section", "port")Typed reads through getint, getfloat and getbooleanconfig.getint("server", "port")
config["DEFAULT"]["key"]Defaults inherited by every sectionconfig["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

  1. 1Check whether the file starts with a BOM

    head -c 3 config.ini | xxd

    A leading efbb bf means there is a UTF-8 BOM and the file needs re-saving without one.

  2. 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.

  3. 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.

  4. 4Dump the raw lines to check comment markers and quoting

    grep -n '^\s*['\";#[]' config.ini

    The output lets you compare section headers, comment lines and value syntax one by one, and anything odd stands out immediately.

  5. 5Confirm which Git configuration is actually in effect

    git config --list --show-origin

    The 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.1

DEFAULT 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 comment

Trailing 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\\App

The 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