52.7 INI/CFG: configparser
While JSON, YAML, and TOML are modern favorites for configuration, the INI file format remains a stalwart in the computing world, particularly within the Python ecosystem due to its simplicity and long-standing Windows legacy. The configparser module in Python’s standard library provides a powerful and intuitive way to work with these files. It’s important to understand that configparser does not parse the Windows Registry format, but rather the classic INI style consisting of sections, properties, and values.
The Structure of an INI File
An INI file is organized into sections, each declared by a header enclosed in square brackets []. Every section contains key-value pairs, also known as options. The format is intentionally simple, lacking support for complex nested data structures natively. configparser extends this basic format slightly, most notably by allowing interpolation—using values from other options within the file.
; config.ini
; Comments are marked with a semicolon or hash
[database]
host = db.example.com
port = 3306
username = admin
password = secret ; Values can contain spaces
[paths]
data_dir = /var/lib/app/data
log_file = %(data_dir)s/app.log ; Interpolation: uses 'data_dir' from this section
Reading Configuration Files
The ConfigParser class reads the file, handles interpolation, and provides a dictionary-like interface to access the values. A critical behavior to note is that by default, configparser will convert all option names to lowercase. This can be disabled by passing optionxform=lambda option: option to the constructor.
import configparser
config = configparser.ConfigParser()
config.read('config.ini') # Returns a list of successfully read files
# Access values. The 'get' methods automatically convert to appropriate types.
db_host = config['database']['host']
db_port = config['database'].getint('port') # Returns an integer
# Using the get method with a fallback default value
db_timeout = config['database'].getfloat('timeout', fallback=10.0)
print(f"Database will timeout after {db_timeout} seconds.")
# Accessing interpolated values
log_path = config['paths']['log_file']
print(f"Log file is at: {log_path}") # Outputs: /var/lib/app/data/app.log
Writing Configuration Files
configparser can also write configuration files, which is useful for creating default configurations or saving user-modified settings. The write method outputs the configuration to a file object.
# Create a new configuration
new_config = configparser.ConfigParser()
new_config['DEFAULT'] = {'version': '1.0', 'debug': 'False'} # DEFAULT section is special
new_config['user'] = {
'name': 'Alice',
'language': 'en',
'access_level': '7'
}
with open('new_config.ini', 'w') as configfile:
new_config.write(configfile)
This creates a file new_config.ini with the following content. Note how the DEFAULT section is not written but its values are propagated to all other sections.
[user]
name = Alice
language = en
access_level = 7
version = 1.0
debug = False
Interpolation: Power and Pitfalls
Interpolation is a powerful feature that allows values to reference other values, using a syntax like %(referenced_option)s. This can be within the same section or, crucially, from the special DEFAULT section. While convenient, this can be a major pitfall. If an interpolated value references a non-existent option, a InterpolationMissingOptionError will be raised. For more robustness and security (to prevent reading unwanted files from interpolation like %(home)s), consider using BasicInterpolation or disabling it entirely with interpolation=None.
# Using BasicInterpolation which only allows references within the same section
from configparser import ConfigParser, BasicInterpolation
safe_config = ConfigParser(interpolation=BasicInterpolation())
safe_config.read_string("""
[paths]
root = /home/user
data = %(root)s/data
""")
print(safe_config['paths']['data']) # Works: /home/user/data
# This would fail with BasicInterpolation but work with the default AdvancedInterpolation
# safe_config.read_string("""
# [DEFAULT]
# root = /home/user
# [paths]
# data = %(root)s/data
# """)
Best Practices and Common Issues
- Type Handling:
configparserreturns all values as strings. You must use the.getint(),.getfloat(), and.getboolean()methods for type conversion. For complex types, you must deserialize the string yourself (e.g., usingjson.loads()on a value). - The DEFAULT Section: Options in the
DEFAULTsection (must be uppercase) are available in all other sections. This is useful for setting universal defaults but can cause confusion if a section seems to have an option you never explicitly defined there. - Case Sensitivity: Be mindful of the
optionxformbehavior. If you need case-sensitive keys, you must override it in the constructor. - Missing Values: Always use the
.get()method with afallbackvalue instead of direct key access ([]). Direct access will raise aKeyErrorif the section or option does not exist, while.get()will gracefully return the fallback. - File Encoding: On Windows, INI files are often assumed to be in the system encoding. For cross-platform compatibility, explicitly specify encoding when reading:
config.read('config.ini', encoding='utf-8').