The basics
What is an INI file?
An INI file stores settings as text: section names in brackets, and under each one, lines of key = value. The name comes from "initialization": programs read the file when they start. It's easy to read and edit by hand, which is why the format has survived since the MS-DOS era.
You'll find INI files, or files in the same style with another extension, all over the place:
| File | Read by |
|---|---|
php.ini | PHP, for every setting from memory limits to upload sizes |
desktop.ini | Windows Explorer, for a folder's icon and display settings; hidden by default |
my.cnf, my.ini | MySQL and MariaDB option files |
setup.cfg, tox.ini, pytest.ini | Python tools, through the configparser dialect |
.gitconfig, .git/config | Git, in its own INI-like dialect |
.npmrc | npm, parsed with the ini package |
.editorconfig | Editors that follow EditorConfig |
*.desktop, *.service | Linux desktop launchers and systemd units, both INI-style |
| Game and app settings | Many Windows programs and games still keep options in an .ini file next to the program or in the user's AppData folder |
- Not a standard. There's no specification, so each program decides what counts as a comment, whether quotes mean anything and what happens to duplicates.
- Not typed. Every value is text. Whether
false,0oroffmeans "no" is up to the program. - Not nested. Two levels, sections and keys. Anything deeper is a convention of one particular parser.
Anatomy
What's inside an INI file
An INI file has two levels: sections, named in square brackets, and keys with values inside them. That's the whole common core; everything else depends on the program reading it. Here is a small file, part by part.
; Settings for a small web app
[app]
name = Sample web app
debug = false
port = 8080
[database]
host = db.example.internal
port = 5432
password =
# Paths need no escaping
[paths]
data_dir = C:\ProgramData\SampleApp
[mail server]
smtp.host = mail.example.internal- 1
; Settings for a small web appA comment
A line starting with a semicolon is a comment in every INI dialect.
- 2
[app]A section header
A name in square brackets. Every key below it, up to the next header, belongs to this section. Sections can't be nested.
- 3
name = Sample web appA key and a value
Everything after the
=, minus surrounding spaces, is the value. No quotes are needed for text with spaces. - 4
debug = falseA value that looks like a boolean
In INI it's the text
false. Python's configparser keeps it as text until you callgetboolean(); theinipackage for Node.js returns the booleanfalse; PHP returns an empty string. - 5
port = 8080A number, as text
Also text, converted by the program.
portcan appear again in another section: keys only have to be unique within their section. - 6
password =An empty value
Accepted by most parsers, which return an empty string. A key with no
=at all is another matter: some parsers accept it, others report an error. - 7
# Paths need no escapingA hash comment
Python, Git and the
inipackage treat#as a comment too, but PHP's parser doesn't. Stick to;in files that different programs read. - 8
data_dir = C:\ProgramData\SampleAppA Windows path
Backslashes are ordinary characters in most dialects. Git's config format is an exception, where a backslash starts an escape.
- 9
[mail server]A section name with a space
Allowed by most parsers. Git's config format isn't one of them: it writes subsections as
[remote "origin"]instead. - 10
smtp.host = …A dotted key
To most parsers the dot is just part of the name. A few, such as the
inipackage for section names, read dots as nesting.
Background
Where INI files come from
INI files became widespread with MS-DOS and 16-bit Windows. Up to Windows 3.1x, Windows itself kept its configuration in INI files such as WIN.INI and SYSTEM.INI, and programs read their own with Windows API functions like GetPrivateProfileString. Those functions look up a value by section and key, ignoring upper and lower case.
From Windows 95 and Windows NT on, Microsoft moved system settings into the registry. The INI functions still exist, but Microsoft's documentation now says they are provided only for compatibility with 16-bit applications and that applications should store settings in the registry. Windows even redirects some INI files to the registry through the IniFileMapping registry key, so editing those files has no effect.
Outside Windows the format lived on because it's so simple. Python, PHP, Git, systemd and many others adopted INI-style files, each with its own parser and its own small differences, which is where most of today's confusion comes from.
Quirks
One file, four different readings
sample-parser-differences.ini contains eight ordinary-looking lines. Python's configparser, the ini package for Node.js and PHP's parse_ini_file all read it without an error, and all return something different:
| Line | Python configparser | ini (Node.js) | PHP parse_ini_file |
|---|---|---|---|
port = 8080 ; the default | "8080 ; the default" | "8080" | "8080" |
path = /var/www # web root | "/var/www # web root" | "/var/www" | "/var/www # web root" |
banner = " Welcome " | Quotes kept: " Welcome " | " Welcome " | " Welcome " |
enabled = true | "true" (text) | true (boolean) | "1" |
Timeout = 30 | Key becomes timeout | Key stays Timeout | Key stays Timeout |
discount = 15%% | "15%" | "15%%" | "15%%" |
tags[] = web | A key called tags[] | A list: ["web"] | A list: ["web"] |
[app.cache] | A section called app.cache | Nested: app → cache | A section called app.cache |
Git's git config reads the first six lines (strips both inline comments and the quotes, lowercases Timeout) and then stops with bad config line, because tags[] isn't a valid Git key. The other differences that matter in practice:
- Duplicate keys. Python's configparser stops with
DuplicateOptionErrorby default (and because it lowercases keys,Portandportcount as duplicates). Theinipackage and PHP keep the last value. Git keeps all of them as a multi-valued key. - Keys before the first section. configparser reports
MissingSectionHeaderErrorunless you passallow_unnamed_section=True(Python 3.13 and later). Theinipackage and PHP put them at the top level. - Reserved words and characters in PHP.
null,yes,no,true,false,on,offandnonecan't be keys. In unquoted values, characters such as&,|,~and!have special meanings, and some, such as=and(, are syntax errors. - Multi-line values. configparser continues a value on indented lines below it. The
inipackage reads those lines as new keys, and PHP ignores them.
Writing INI files that every parser reads the same way
Use ; comments on their own lines, one key per name per section, a section header before the first key, no quotes, no inline comments and only letters, digits, _, - and . in names. sample-config.ini follows these rules.
Characters
Encoding, byte order marks and line endings
Because there's no standard, nothing says which encoding an INI file uses. Old Windows programs read and write them in the system's ANSI code page; newer software expects UTF-8. Three things go wrong regularly:
- A byte order mark (BOM). Some Windows editors put three invisible bytes,
EF BB BF, at the start of a UTF-8 file. Python's configparser, reading withencoding="utf-8", then sees\ufeff[greetings]instead of a section header and stops withMissingSectionHeaderError; reading withencoding="utf-8-sig"works. Theinipackage is worse: it raises no error but turns the first section header into a key, so the first section's settings end up at the top level. PHP and Git ignore the BOM. - The wrong code page. Without an explicit
encoding, Python opens the file with the system's default encoding, which on many Windows machines is not UTF-8. Accented text then comes out asé. Always pass the encoding. - Line endings. All the parsers tested here accept Windows (CRLF) and Unix (LF) line endings.
sample-utf8-bom-crlf.ini has a BOM, CRLF line endings and non-ASCII keys and values, so you can see which of these problems a tool has.
How it's used
Reading an INI file from a program
import configparser
config = configparser.ConfigParser()
config.read("settings.ini", encoding="utf-8-sig") # also accepts a BOM
port = config.getint("database", "port") # "5432" -> 5432
debug = config.getboolean("app", "debug") # "false" -> False
host = config.get("database", "host", fallback="localhost")- Python:
configparserin the standard library. Values are strings;getint(),getfloat()andgetboolean()convert them, andgetboolean()accepts1/0,yes/no,true/falseandon/off. Keys in a[DEFAULT]section apply to every section, and%(name)srefers to another key, which is why a literal%must be written%%. - Node.js: the
inipackage. It returnstrue,falseandnullas real values, collectskey[]lines into arrays and nests dotted section names. - PHP:
parse_ini_file($path, true)keeps sections. By defaulton,yesandtruebecome"1"; passINI_SCANNER_TYPEDto get real booleans,nulland integers. - Windows (C, C++, C#):
GetPrivateProfileStringand related functions still work, case-insensitively, but Microsoft recommends the registry or a modern format for new code. - Git-style files:
git config -f file.ini section.keyreads a value from any file in Git's dialect.
Comparison
INI vs TOML, YAML and JSON
| INI | [TOML](/file-types/toml) | [YAML](/file-types/yaml) | [JSON](/file-types/json) | |
|---|---|---|---|---|
| Specification | None | Yes: TOML 1.1.0 | Yes: YAML 1.2.2 | Yes: RFC 8259 |
| Looks like | [section] and key = value | [table] and key = value | Indented key: value | Braces and quotes |
| Types | Everything is text | Strings, numbers, booleans, dates, arrays | Guessed from the text | Strings, numbers, booleans, null, arrays, objects |
| Nesting | Two levels | Any depth | Any depth | Any depth |
| Comments | ; and often # | # | # | None |
| Same result in every parser | No | Yes | Mostly, if you stick to YAML 1.2 | Yes |
If you're choosing a format for a new project and like the look of INI, TOML is the closest thing with a specification: a simple INI file with # comments and quoted string values is often already valid TOML. Keep INI where a program expects it, such as php.ini or desktop.ini.
Troubleshooting
Why won't my INI file load?
| Message | Read by | Usual cause and fix |
|---|---|---|
MissingSectionHeaderError: File contains no section headers. | Python | Keys before the first [section], or a BOM. Add a header, or read with encoding="utf-8-sig" |
DuplicateOptionError: … option 'port' in section 's' already exists | Python | The same key twice in a section, including Port and port. Remove one, or pass strict=False to keep the last |
InterpolationSyntaxError: '%' must be followed by '%' or '(' | Python | A literal % in a value. Write %%, or create the parser with interpolation=None |
ParsingError: Source contains parsing errors | Python | Often a line with a key but no =. Add = value, or pass allow_no_value=True |
syntax error, unexpected '=' (or '(', '!') | PHP | A special character in an unquoted value. Put the value in double quotes |
syntax error, unexpected BOOL_TRUE (or NULL_NULL) | PHP | A reserved word such as on or null used as a key. Rename the key |
fatal: bad config line 21 | Git | A name Git doesn't allow, such as one with a space or [], or an invalid backslash escape |
| Settings are ignored, no error | Any | The key is in the wrong section, the program reads a different file (php --ini lists the files PHP loads), or the program must be restarted |
How to
How to open and edit an INI file
- Windows: right-click and choose Open with, then Notepad or another text editor. Some
.inifiles sit in protected folders such as Program Files; open the editor as administrator to save changes there. - macOS and Linux: open it in TextEdit (in plain-text mode), VS Code or any terminal editor such as
nano. - desktop.ini: it's a hidden system file, so Explorer only shows it when "Hide protected operating system files" is turned off in Folder Options.
- php.ini:
php --iniprints which file PHP loaded. After editing it, restart PHP-FPM or the web server. - Convert to JSON: load it with a parser and write JSON, but remember the result depends on the parser, as the table above shows.
Security
Is an INI file safe? Passwords in config files
An INI file is only text and can't run anything when opened. The risk is what's written in it: INI files often hold credentials in plain text.
- Database passwords in
my.cnfor an app'sconfig.ini. Restrict who can read the file, such aschmod 600on Linux and macOS. - Tokens in
.npmrc(registry auth tokens) and in.git/configwhen a remote URL includes a username and token. - Files inside the web root. A
config.iniin a public folder can be downloaded by anyone who guesses its name. Keep config files outside the folder the web server publishes, or block.inifiles in the server's configuration. - Shared copies. Before posting an INI file in a forum, ticket or link, remove passwords, keys and internal hostnames.
Never commit a real config.ini with secrets to a repository. Commit an example file with placeholders, such as config.example.ini, and keep the real one out with .gitignore.
Free downloads
Sample INI files to download
Free sample INI files for testing parsers, config loaders, encodings and upload limits. Each one loads in Python's configparser (the BOM file with encoding="utf-8-sig"), the ini package for Node.js and PHP's parse_ini_file.
Loading…What's inside sample-config.ini
The file is commented: each numbered comment marks one of these parts. It sticks to the INI that Python, Node.js and PHP read alike.
Comment 1: comments
Only
;comments, on their own lines, because PHP doesn't treat#as a comment.Comments 2 and 3: sections and keys
A
[app]header andkey = valuelines, with spaces around=that every parser trims.Comments 4 and 5: values are text
debug = falseand an emptyowner =. The parsers agree on the empty value but not onfalse: text in Python, a boolean in Node.js, an empty string in PHP.Comment 6: keys per section
portappears in both[app]and[database], which is fine: keys only need to be unique within a section.Comments 7 and 8: punctuation and backslashes
A value with a colon, a comma and a slash, and a Windows path with backslashes, all without quotes.
Comment 9: lists
Comma-separated values, which your program has to split itself.
Comment 10: names with spaces and dots
[mail server]andsmtp.host, accepted by Python, Node.js and PHP. Git's config reader rejects them, and also stops earlier, at the backslashes in comment 8.
All sample INI files
Commented config file
sample-config.ini · 1.3 KB (1,368 bytes)
An app configuration with 10 numbered comments: sections, keys, empty values, a Windows path, comma-separated lists and names with spaces and dots, written so that configparser,
iniand PHP read the same strings.- Teaching the parts of an INI file
- Testing config loaders
Where parsers disagree
sample-parser-differences.ini · 771 bytes
Inline
;and#comments, a quoted value,true, a capitalised key,%%, akey[]list and a dotted section name. Parsers read it without errors and return the different results in the table above.- Finding out which dialect your parser follows
- Comparing parsers across languages
UTF-8 with BOM, CRLF and non-ASCII text
sample-utf8-bom-crlf.ini · 602 bytes
Starts with a UTF-8 byte order mark directly before the first
[section], uses Windows line endings, and has greetings in six scripts, emoji, and accented section and key names.- Testing BOM handling
- Spotting encoding bugs
php.ini-style settings
sample-php-style.ini · 900 bytes
Real PHP directive names with example values in
[PHP]and[Session]sections: size suffixes,On/Offswitches and quoted values with special characters.- Testing tools that read or edit `php.ini`
- Seeing PHP's quoting rules and `On`/`Off` conversion
100 KB INI file
sample-100kb.ini · 100 KB (102,400 bytes)
1,042 sections of fictional device settings, padded with a final comment to exactly 102,400 bytes.
- Upload and size-limit tests
- Parser performance
Ways to test with them
- Which dialect a parser follows: load
sample-parser-differences.iniand compareport,bannerandenabledwith the table above. - BOM handling: load
sample-utf8-bom-crlf.ini. A correct reader returns agreetingssection withde = Grüße. A[greetings]key at the top level, or a missing-section error, means the BOM wasn't stripped. - PHP's rules:
parse_ini_fileturnsdisplay_errors = Offinsample-php-style.iniinto an empty string andlog_errors = Oninto"1". - Upload limits:
sample-100kb.iniis exactly 102,400 bytes, for testing a limit at the boundary.
Free to use for testing, teaching and demos, commercially or not, with no attribution needed. A link back to this page is always appreciated.
Put it online
Have an INI file? Give it a link.
Drop an INI file on 99helpers and it gets its own address, for sharing a config example or a test fixture. Opened in a browser, the link shows the file as plain text. Remove passwords and keys before you share it.
Questions
INI files, frequently asked
What is an INI file?
A plain-text settings file with [sections] and key = value lines. Programs read it at start-up; the name comes from "initialization".
How do I open an INI file?
With any text editor: Notepad on Windows, TextEdit in plain-text mode on a Mac, or VS Code anywhere. Double-clicking usually opens it in Notepad on Windows.
Is it safe to delete desktop.ini?
Yes, it only stores how a folder looks, such as its icon or display name. Windows may recreate it when you change those settings again.
Can INI files have comments?
Yes. A line starting with ; is a comment in every dialect. Many parsers also accept #, but PHP's doesn't, and comments after a value on the same line are handled differently by different parsers.
Are INI keys case-sensitive?
It depends on the parser. Windows' own functions and Python's configparser ignore case in keys; the ini package for Node.js and PHP don't. Git ignores case in section and key names but not in subsection names.
Is there an INI standard?
No. The format grew out of MS-DOS and Windows, and every parser has its own rules for comments, quotes, duplicates and encoding. TOML is a newer format in the same style that does have a specification.
How do I read an INI file in Python?
Use configparser from the standard library: config.read("file.ini", encoding="utf-8"), then config["section"]["key"] or getint() and getboolean() for typed values.
Should I use INI or TOML for a new project?
TOML, if you have the choice: it looks similar but has a specification and real types, so every parser reads it the same way. Use INI where a program requires it.
Related formats
Other file types explained
Keep going
Related
Sources
- Python documentation: configparser
- Microsoft: GetPrivateProfileString function
- PHP manual: parse_ini_file
- Git documentation: git-config, configuration file syntax
- INI file (Wikipedia): history and dialects
Last reviewed 27 October 2026.