88.3 configparser: INI-Style Configuration Files
Right, so you need to configure your application. You could hardcode everything, but then you’d have to change your code every time a database password changes, and frankly, you and I both know that’s a path that leads to tears and a git revert at 2 AM. We need something better. We need config files.
And in the Python world, when you think “config file,” you probably think of the humble INI file. It’s the reliable beige sedan of configuration formats: not flashy, but it gets the job done and everyone knows how to drive it. The standard library module for handling these is configparser, and despite its name, it’s not just for parsing—it’s for reading and writing.
Let’s get one thing straight immediately: configparser does not, I repeat, does not parse the classic Windows INI file format. The name is a lie, a beautiful and useful lie. It uses a format that looks like an INI file, but it’s more powerful and consistent. If you throw a legacy INI with drive letters (C:) or no-section values at it, it’ll choke. And honestly, that’s for the best.
The Basic Vibe: Sections, Keys, and Values
An INI-style config file is broken into sections, denoted by square brackets []. Inside each section are key-value pairs. The configparser object you create acts like a nested dictionary: the top-level keys are your section names, and the value for each of those is another dictionary of the key-value pairs inside that section.
Let’s create a config file for our world-dominating app, app.conf:
[database]
host = db.example.com
port = 5432
username = admin
password = supers3cret # Don't worry, we'll talk about this later.
encoding = utf-8
[feature_flags]
enable_telemetry = false
new_ui = true
[paths]
home = /home/myapp/
data = /home/myapp/data
Now, let’s read that in Python. It’s almost embarrassingly simple.
import configparser
config = configparser.ConfigParser()
config.read('app.conf')
db_host = config['database']['host']
db_port = config['database']['port']
print(f"Connecting to {db_host}:{db_port}")
# Output: Connecting to db.example.com:5432
Notice something crucial? The port value came back as a string '5432', not an integer. configparser is aggressively, hilariously string-oriented. Everything is a string by default. This is its most common gotcha. You are responsible for converting to the appropriate data type.
Data Types and the getint, getfloat, getboolean Lifesavers
Because the “everything is a string” paradigm is a fantastic way to introduce subtle bugs, configparser provides helper methods to avoid them. These methods are attached to each section proxy.
# The risky way (you get a string)
port_str = config['database']['port']
print(type(port_str)) # <class 'str'>
# The safe way (you get an actual integer)
port_int = config['database'].getint('port')
print(type(port_int)) # <class 'int'>
# Same for floats and, very importantly, booleans
enable_telemetry = config['feature_flags'].getboolean('enable_telemetry')
print(enable_telemetry, type(enable_telemetry)) # False <class 'bool'>
It correctly handles true/false, yes/no, on/off, and even 1/0. Use it. Your sanity will thank you.
Interpolation: The Coolest Feature You Might Not Need
Here’s a feature that’s either brilliant or infuriating: interpolation. It allows values to reference other values, like variables. The classic use case is building paths.
[paths]
home = /home/myapp/
data = %(home)s/data/logs
To use this, you must use the get() method and not the direct dictionary lookup.
config = configparser.ConfigParser(interpolation=configparser.ExtendedInterpolation())
config.read('app.conf')
data_path = config['paths'].get('data')
print(data_path) # /home/myapp/data/logs
Is it cool? Absolutely. Is it a potential maintenance nightmare if you go overboard? You bet. Use it with caution. For most cases, I find it cleaner to just do the string interpolation in my Python code (f"{config['paths']['home']}/data") and keep the config file dumb and simple.
Writing Configs: Because You’re Not Just a Consumer
You can also create or modify configs programmatically and write them back out. This is great for building tools that set up configuration for users.
config = configparser.ConfigParser()
config['DEFAULT'] = {'debug': 'true', 'timeout': '30'}
config['user_1'] = {}
config['user_1']['name'] = 'Alice'
config['user_2'] = {'name': 'Bob'}
with open('new_config.ini', 'w') as configfile:
config.write(configfile)
This creates a file that looks like this. Note the DEFAULT section—a special section whose values are available in all other sections unless they’re explicitly overridden.
[DEFAULT]
debug = true
timeout = 30
[user_1]
name = Alice
[user_2]
name = Bob
The Security Elephant in the Room
See that password in our first example? Yeah, storing plaintext secrets in a file next to your code is a terrible idea. configparser is for configuration, not secrets. For that, you need a proper secrets manager, environment variables, or at the very least, a file with strict permissions that’s never checked into version control. configparser doesn’t encrypt or protect anything. It just reads text files.
So, in summary: use configparser for what it’s good at—simple, hierarchical configuration with clear sections. Use its getint and getboolean methods to avoid type errors. Be very skeptical of interpolation. And for the love of all that is holy, keep your secrets out of it. It’s a trusty tool, not a magician.