INI Files: The Oldest Config Format Still in Use

INI Files: The Oldest Config Format Still in Use

INI files are older than most of the developers using them. They predate XML, predate JSON, predate YAML by decades. And yet here we are in 2026 and you'll still find .ini files in fresh codebases, Git repos, and server configs everywhere. There's a reason for that.

Where INI Came From

The format goes back to Windows 3.1, where Microsoft used .ini files for per-application settings. WIN.INI and SYSTEM.INI configured the whole operating system — see the Wikipedia entry on INI files for the historical timeline. The format was chosen for its simplicity: a plain text file any text editor could open, readable without documentation, writable by hand without tooling.

Similar formats existed on DOS systems before Windows 3.1. The convention was already well-established by the time it got the "INI" name — and that format has been in continuous use for over 35 years.

Windows NT and later moved toward the registry for system settings, but .ini files stuck around for application-level config. They're also the basis for formats that came later — Git's config format, .editorconfig, Python's configparser module. The lineage is direct.

The Structure: Sections, Keys, and Values

An INI file has three building blocks.

Sections are declared with a name in square brackets. Everything below a section header belongs to that section, until the next section header or end of file.

Keys (sometimes called "properties" or "options") are names on the left side of an equals sign. Values are on the right. Whitespace around the = is typically ignored.

Comments start with a semicolon ; or a hash #, depending on the parser. Everything after the comment character on that line is ignored.

; Database connection settings
[database]
host = localhost
port = 5432
name = myapp_production
pool_size = 10

[cache]
backend = redis
url = redis://localhost:6379/0
ttl = 3600

[logging]
level = info
# Some parsers treat # as comments too
file = /var/log/myapp.log

That's essentially the whole format. No arrays, no nested objects, no type system. Just sections, keys, and string values.

The Problem: There Is No Standard

INI has no formal specification. No RFC, no schema validator, no authoritative grammar. Every parser implements its own dialect — and that trips up developers coming from JSON or YAML.

Consider a few questions that different parsers answer differently:

Are keys outside any section valid? Some parsers treat them as belonging to a default section. Others throw a parse error.

Are duplicate keys allowed? Some parsers take the last value. Some take the first. Some merge them into a list.

Are values case-sensitive? Keys often get lowercased automatically. Values might or might not.

What's the comment syntax? Python's configparser supports both ; and #. Windows APIs only recognized ;. Git's INI parser uses # and ;.

Can you quote values? Some parsers strip quotes from value = "something", treating it as the string something. Others keep the quotes as part of the value.

If you're reading INI files written by other tools, test it. Don't assume your parser and theirs agree on edge cases.

Python's configparser

Python ships with configparser in the standard library, and it's the most commonly used INI parser in modern code. The defaults are sensible, and it handles the common dialect well.

import configparser

config = configparser.ConfigParser()
config.read('settings.ini')

# Access values
host = config['database']['host']
port = config.getint('database', 'port')  # type coercion
ttl = config.getfloat('cache', 'ttl')

# Default values
log_level = config.get('logging', 'level', fallback='warning')

# Write a config file
config['app'] = {
    'version': '2.1.0',
    'debug': 'false'
}
with open('output.ini', 'w') as f:
    config.write(f)

One quirk: configparser stores all keys in lowercase by default. If your INI file has MaxConnections = 100, you read it as config['section']['maxconnections']. Override this with a custom optionxform if you need case-sensitive keys.

Python's configparser also supports interpolation — referencing other values within the file using %(key)s syntax:

[paths]
base = /opt/myapp
logs = %(base)s/logs
data = %(base)s/data

This is a configparser-specific feature, not universal INI behavior.

Git Config Is INI

If you've used Git, you've used INI without necessarily knowing it. The .git/config file, ~/.gitconfig, and /etc/gitconfig are all INI format — the exact dialect is documented in git-config(1).

[core]
    repositoryformatversion = 0
    filemode = true
    bare = false

[remote "origin"]
    url = git@github.com:user/repo.git
    fetch = +refs/heads/*:refs/remotes/origin/*

[branch "main"]
    remote = origin
    merge = refs/heads/main

Git's INI dialect supports subsections — the "origin" part in [remote "origin"] is a quoted subsection name. That's not universal INI behavior, it's Git-specific. But it shows how parsers can extend the base format without breaking the overall readability.

.editorconfig Is INI Too

.editorconfig files — used to enforce consistent coding style across editors and IDEs — also use INI syntax. Section names are glob patterns instead of arbitrary names, but the format reads identically. The official EditorConfig spec lists the recognized properties.

root = true

[*]
indent_style = space
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false

[Makefile]
indent_style = tab

Two completely different tools — Git and EditorConfig — independently chose INI format. That says something about its staying power. It's readable, it's writable, and every developer can understand it on first sight.

Multiline Values

Standard INI doesn't have a native multiline syntax, but many parsers support continuation lines — a value that spans multiple lines by indenting the continuation.

Python's configparser does this:

[email]
recipients =
    alice@example.com
    bob@example.com
    charlie@example.com

The value of recipients becomes a single string with newlines. You'd split it yourself to get the list. Again, this is parser-specific behavior, not universal INI.

INI vs TOML vs YAML

When should you use INI over the alternatives?

INI wins when the config is truly flat — a handful of sections, simple key-value pairs, edited by humans unfamiliar with markup syntax. Git config and .editorconfig are perfect examples. Non-technical users can edit an INI file without training. The format has no footguns.

TOML wins when you need types. TOML has booleans, integers, floats, dates, arrays, and inline tables, all with explicit syntax. You don't have to manually coerce "3600" to an integer. TOML is where INI-style configs logically go when they grow up. Read TOML: The Config Format Built to Replace INI for the full comparison.

YAML wins when you need deeply nested structures or you're already in a YAML-heavy ecosystem (Kubernetes, Ansible, GitHub Actions). YAML's expressiveness is unmatched — and so is its potential for confusion. For simple configs, YAML is overkill.

The honest answer is: if your config file is going to stay flat, INI is perfectly fine. If it's going to grow nested structures or typed values, start with TOML.

When INI Is Still the Right Call

Some teams reach for JSON or YAML for configs where INI would work better. INI is worth choosing when:

  • The file will be edited by hand frequently, by people who aren't developers
  • The config is genuinely flat — no nesting needed
  • You want zero external dependencies (INI parsers are in every standard library)
  • Compatibility with an existing ecosystem using INI (Git hooks, .editorconfig, PHP php.ini, MySQL my.cnf)
  • You want comment support in your config (JSON doesn't have it without a superset)

The format's longevity is a feature, not a liability. A config format that non-programmers can read and edit without a tutorial has real value.

Wrapping Up

INI files are simple by design, and that simplicity is why they've outlasted dozens of "better" alternatives. No spec, no schema validator — just sections and keys. When you need to understand or debug a config in this format, the JSON Formatter can help you work through related structured data, and the JSON to YAML converter is useful if you're migrating a flat INI config to a more structured format. The configparser docs are the definitive reference for how Python handles INI parsing.

FAQ

Is INI a real standard or just a convention?

It's a convention — there's no formal RFC, no ISO standard, no authoritative grammar. Different parsers handle edge cases differently: comment characters (; vs #), case sensitivity, duplicate keys, multi-line values, and quoting all vary. The "INI format" is whatever your specific parser implements. This is why TOML exists — it's an INI-shaped format with a real specification.

Should I use INI or TOML for new projects in 2026?

TOML for anything new. TOML is an explicitly specified format with the same readable shape as INI, but with proper types (booleans, integers, floats, dates, arrays, inline tables) and unambiguous parsing rules. INI is fine for legacy ecosystems (Git, EditorConfig) but creates problems when configs grow or move between tools. Cargo, pyproject.toml, and most modern Rust/Python tooling use TOML for this reason.

Why does configparser lowercase my keys?

Python's configparser calls str.lower() on keys by default to make lookups case-insensitive (MaxConnections becomes maxconnections). To preserve case, override optionxform: config.optionxform = str. This matches the historical Windows INI behavior, which was case-insensitive. Most other INI parsers (Git, EditorConfig) preserve case by default.

Can INI files have nested sections?

Standard INI doesn't support nesting — sections are flat. Some parsers extend it with conventions: Git uses subsection syntax [remote "origin"], others use dot notation [parent.child] (which is actually TOML). If you need nesting, switch to TOML or YAML; trying to fake it in INI leads to inconsistent parsing across tools.

How do I handle arrays in INI files?

Standard INI has no array type. Common workarounds: comma-separated values (hosts = a.com,b.com,c.com), repeated keys (some parsers like Java's Properties merge them, others overwrite), or continuation lines (Python's configparser supports indented multi-line values that you split yourself). All of these are parser-specific. If you need arrays as a first-class type, use TOML or JSON.

Why do INI files use both `;` and `#` for comments?

Historical accident. The original Windows INI convention used ;, while Unix-derived tools (Bash, Python, Git) preferred #. Most modern parsers accept both for compatibility. If you're authoring INI files for cross-platform use, prefer # for inline comments and ; only when the parser doesn't support # (rare in 2026).

What's the maximum size for an INI file in practice?

There's no theoretical limit, but parsers typically load the whole file into memory, so practical limits are around 10-100 MB depending on the parser. INI is designed for human-edited configuration, not bulk data — if your config is bigger than a few hundred KB, that's a sign you should switch to a database or a structured format like SQLite.

How do I convert INI to YAML or JSON?

Most languages can do this in 5-10 lines: parse INI with the standard library (configparser in Python, ini-parser in Node), then serialize the resulting dict to JSON or YAML. The catch is that INI's flat structure doesn't map cleanly onto JSON's nested shape — you'll get {section_name: {key: value}} rather than deeply nested objects. For one-off conversions, online tools work fine; for production migrations, write a script that tests round-trip parsing.