INI File Format: Configuration Files Explained
The INI file format is one of the oldest and most widely recognized configuration file formats in computing. Despite having no formal standard and dozens of incompatible dialects, INI files remain ubiquitous across Windows systems, PHP applications, game engines, database servers, and development tools. Their appeal is immediately obvious: plain text, human-readable, and simple enough to edit without any specialized tools. This guide covers the structure, dialects, parsing, limitations, and the modern landscape of alternatives that have emerged to address INI's shortcomings.
History and Origins
The INI format takes its name from the .ini extension used by Windows initialization files. The format dates to the late 1980s with early versions of Windows, which used WIN.INI and SYSTEM.INI to store system configuration. DOS applications had used similar flat key=value text files even earlier, but Windows popularized the sectioned structure that defines INI today.
Microsoft moved away from INI files in Windows 95, replacing them with the Windows Registry for system configuration. However, the format lived on in application-level configuration — php.ini, my.cnf (MySQL), smb.conf (Samba), Thumbs.ini, game settings files, and countless developer tools continue to use INI-style syntax today.
Basic Structure
An INI file is a plain text file organized into sections, keys, and values:
; This is a comment
# This is also a comment (in many dialects)
[database]
host = localhost
port = 5432
name = production_db
user = app_user
password = s3cr3t
[server]
host = 0.0.0.0
port = 8080
debug = false
workers = 4
[logging]
level = info
file = /var/log/app.log
rotate = daily
max_size = 100MB
The structure is intuitive:
- Sections — delimited by
[section_name]on its own line. Section names are typically case-insensitive (though parsers vary). - Keys — the name of a configuration item, to the left of the
=or:separator. - Values — the right side of the separator. Whitespace around the separator is typically ignored.
- Comments — lines beginning with
;or#are comments (parser-dependent;#is not universally recognized).
INI Dialects: The Lack of a Standard
Unlike JSON or TOML, INI has no single governing specification. Every parser implements its own rules. This creates significant portability challenges:
Comment Characters
| Parser | ; comments |
# comments |
|---|---|---|
Python configparser |
✅ | ✅ (by default) |
PHP parse_ini_file |
✅ | ✅ |
Windows GetPrivateProfileString |
✅ | ❌ |
MySQL my.cnf |
✅ | ✅ |
| Git config | ✅ | ✅ |
Key-Value Separators
Most parsers accept key = value. Some also accept key: value (Python configparser, Java Properties) or key value (no separator, some Unix configs). Git config accepts key = value within sections.
Multi-line Values
Python configparser supports multi-line values with indentation:
[message]
body = This is line one.
This is line two (indented continuation).
This is line three.
Windows INI (via GetPrivateProfileString) does not support multi-line values at all.
Duplicate Keys
- Python
configparser: last value wins (by default) - Some parsers: first value wins
php.ini: last value wins- No standard behavior — parsers disagree
Global Keys (Before First Section)
Keys defined before any section header are "global" or "rootless". Python configparser requires a [DEFAULT] section header; Windows INI ignores global keys. This is a common source of portability bugs.
Python: configparser
Python's configparser module provides a robust INI parser:
import configparser
config = configparser.ConfigParser()
config.read('settings.ini')
# Read a value
db_host = config['database']['host']
db_port = config.getint('database', 'port') # Type conversion
debug = config.getboolean('server', 'debug')
# Iterate sections
for section in config.sections():
print(f"[{section}]")
for key, value in config.items(section):
print(f" {key} = {value}")
# Write an INI file
config['new_section'] = {
'key1': 'value1',
'key2': '42',
}
with open('output.ini', 'w') as f:
config.write(f)
The DEFAULT section: configparser treats [DEFAULT] specially — its values serve as defaults for all other sections:
[DEFAULT]
timeout = 30
retries = 3
[api_v1]
endpoint = https://api.example.com/v1
; timeout and retries inherited from DEFAULT
[api_v2]
endpoint = https://api.example.com/v2
timeout = 60 ; overrides DEFAULT
config.getint('api_v1', 'timeout') # Returns 30 (from DEFAULT)
config.getint('api_v2', 'timeout') # Returns 60 (overridden)
Case sensitivity: By default, configparser lowercases all keys. To preserve case:
config = configparser.RawConfigParser()
config.optionxform = str # Disable lowercasing
PHP: parse_ini_file
PHP's parse_ini_file() function is built for reading php.ini-style files:
<?php
// Parse into array
$config = parse_ini_file('config.ini', true); // true = process sections
echo $config['database']['host']; // localhost
echo $config['server']['port']; // 8080
// Scan mode for typed values
$config = parse_ini_file('config.ini', true, INI_SCANNER_TYPED);
var_dump($config['server']['debug']); // bool(false) — not string "false"
// Write INI (no built-in — must generate manually)
function write_ini_file(array $data, string $path): void {
$lines = [];
foreach ($data as $section => $values) {
$lines[] = "[$section]";
foreach ($values as $key => $value) {
if (is_bool($value)) {
$lines[] = "$key = " . ($value ? 'true' : 'false');
} elseif (is_array($value)) {
foreach ($value as $item) {
$lines[] = "{$key}[] = $item";
}
} else {
$lines[] = "$key = $value";
}
}
$lines[] = '';
}
file_put_contents($path, implode("\n", $lines));
}
PHP INI arrays: PHP extends the INI format with array support using bracket syntax:
[extensions]
allowed_types[] = jpg
allowed_types[] = png
allowed_types[] = gif
allowed_types[] = webp
This results in $config['extensions']['allowed_types'] being an array ['jpg', 'png', 'gif', 'webp'].
Real-World INI Files
php.ini (PHP Configuration)
[PHP]
engine = On
short_open_tag = Off
precision = 14
output_buffering = 4096
zlib.output_compression = Off
max_execution_time = 30
max_input_time = 60
memory_limit = 128M
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
display_errors = Off
log_errors = On
upload_max_filesize = 2M
post_max_size = 8M
[Date]
date.timezone = UTC
[MySQLi]
mysqli.default_port = 3306
MySQL my.cnf
[mysqld]
user = mysql
datadir = /var/lib/mysql
socket = /var/lib/mysql/mysql.sock
port = 3306
bind-address = 127.0.0.1
max_connections = 151
innodb_buffer_pool_size = 128M
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2
[client]
port = 3306
socket = /var/lib/mysql/mysql.sock
default-character-set = utf8mb4
Git Config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[remote "origin"]
url = https://github.com/user/repo.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main
[user]
name = Pablo Cirre
email = pablo@example.com
Note Git's indentation with tabs (not spaces), quoted section subsections, and case-sensitive key names — all non-standard INI behaviors.
Encoding and Special Characters
INI files are typically UTF-8 encoded in modern applications. Special characters in values are handled inconsistently:
- Quotes: some parsers strip surrounding quotes from values (
"hello"→hello); others include them literally - Backslashes:
\n,\tescape sequences are supported by some parsers (Python configparser, php.ini) but not others - Semicolons in values: inline comments (
; commentafter a value) are supported by some parsers but treated as part of the value in others
; Python configparser — inline comment stripped:
name = John Doe ; this is ignored
; PHP parse_ini_file — inline comment stripped:
name = Jane Doe ; this is also ignored
; Windows INI — no inline comments; the semicolon and text become part of the value
INI vs Modern Alternatives
| Feature | INI | JSON | TOML | YAML |
|---|---|---|---|---|
| Human-readable | ✅ Excellent | ✅ Good | ✅ Excellent | ✅ Excellent |
| Formal standard | ❌ No | ✅ RFC 8259 | ✅ TOML v1.0 | ✅ YAML 1.2 |
| Typed values | ❌ Strings only | ✅ Native types | ✅ Native types | ✅ Native types |
| Arrays | ⚠️ Extension | ✅ Native | ✅ Native | ✅ Native |
| Nested structures | ❌ No | ✅ Unlimited | ✅ Table nesting | ✅ Unlimited |
| Comments | ✅ Yes | ❌ No | ✅ Yes | ✅ Yes |
| Sections | ✅ One level | ❌ Objects only | ✅ Tables + arrays | ❌ Keys only |
| Tooling | ✅ Universal | ✅ Universal | ✅ Good | ✅ Good |
When to Use INI
Despite its age and limitations, INI remains appropriate in specific scenarios:
Choose INI when:
- Editing by non-technical users is expected — INI is the most approachable format
- The configuration is simple and flat (no nested structures)
- The target runtime already uses INI (PHP apps, MySQL, Samba, legacy Windows software)
- Backward compatibility with existing tools is required
Choose TOML instead when:
- You need typed values (integers, booleans, dates, arrays) without ambiguity
- Moderate nesting is needed (one or two levels)
- Starting a new project and INI compatibility is not a constraint
Choose JSON when:
- The configuration will be consumed by many languages and environments
- A formal, unambiguous standard is required
- Comments are not needed (or a JSON superset like JSON5 is acceptable)
Conclusion
The INI format's remarkable staying power comes from its radical simplicity: anyone who can open a text editor can read and edit an INI file. Its lack of a formal standard is simultaneously its greatest weakness (incompatible parsers, dialect fragmentation) and its greatest strength (every environment has implemented its own version, ensuring ubiquity). Understanding INI's structure, the behavioral differences between major parsers, and its limitations compared to TOML and JSON empowers you to make informed format choices for new projects and to safely work with the enormous legacy ecosystem of INI-configured software that continues to run the world's infrastructure.
Related conversions
Frequent conversions across the catalogue: