ZetCode

Python ConfigParser

last modified October 30, 2026 Python 3.15

In this article we show how to work with configuration files in Python with ConfigParser.

Python ConfigParser

ConfigParser is a class from the standard configparser module which implements a basic configuration language. The format is similar to Microsoft Windows INI files. It lets end users customize a program without touching its code.

A configuration file consists of sections followed by key/value pairs. Section names are written in [] brackets. Keys and values are separated with = or :. Comment lines start with # or ;.

ConfigParser stores all values as strings. Keys are case-insensitive, while section names are case-sensitive.

Reading a file

In the first example, we read configuration data from a file.

db.ini
; database connections
[mysql]
host = localhost
user = user7
passwd = s$cret
db = ydb

[postgresql]
host = localhost
port = 5432
user = user8
passwd = mypwd$7
db = testdb

We have two sections of configuration data and a comment.

reading_from_file.py
import configparser

config = configparser.ConfigParser()
found = config.read('db.ini')
print(found)

mysql = config['mysql']

print('MySQL configuration:')
print(f"Host: {mysql['host']}")
print(f"User: {mysql['user']}")
print(f"Database: {mysql['db']}")

pg = config['postgresql']

print('PostgreSQL configuration:')
print(f"Host: {pg['host']}")
print(f"Port: {pg['port']}")
print(f"Database: {pg['db']}")

print(config.read('missing.ini'))

The example reads configuration data for MySQL and PostgreSQL.

config = configparser.ConfigParser()
found = config.read('db.ini')

We create a ConfigParser and read the file with read. The method returns the list of files that were read successfully.

mysql = config['mysql']
print(f"Host: {mysql['host']}")

The parser behaves like a dictionary of sections, and each section like a dictionary of options.

print(config.read('missing.ini'))

A missing file is not an error; read simply returns an empty list. If the file is required, check the return value or open the file yourself and use read_file.

$ python reading_from_file.py
['db.ini']
MySQL configuration:
Host: localhost
User: user7
Database: ydb
PostgreSQL configuration:
Host: localhost
Port: 5432
Database: testdb
[]

Sections and options

The sections method returns the section names, and has_section checks whether a section exists.

sections.py
import configparser

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

print(config.sections())

for name in ['mysql', 'postgresql', 'sqlite']:
    print(name, config.has_section(name), name in config)

for key, value in config['postgresql'].items():
    print(f'{key} = {value}')

print(config.has_option('mysql', 'port'))
print(config.options('mysql'))

We inspect the structure of the db.ini file.

print(name, config.has_section(name), name in config)

The in operator is an alternative to has_section.

for key, value in config['postgresql'].items():

We iterate over the options of a section.

print(config.has_option('mysql', 'port'))
print(config.options('mysql'))

The has_option method checks for an option and options lists the keys of a section.

$ python sections.py
['mysql', 'postgresql']
mysql True True
postgresql True True
sqlite False False
host = localhost
port = 5432
user = user8
passwd = mypwd$7
db = testdb
False
['host', 'user', 'passwd', 'db']

Typed values

Since all values are strings, the parser provides getint, getfloat, and getboolean methods for conversion.

app.ini
[server]
Port = 8080
timeout = 2.5
debug = yes
workers = 4

[cache]
enabled = off

The file contains numbers and boolean flags. The Port key is written with a capital letter.

typed_values.py
import configparser

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

server = config['server']

port = server.getint('port')
timeout = server.getfloat('timeout')
debug = server.getboolean('debug')

print(port, type(port).__name__)
print(timeout, type(timeout).__name__)
print(debug, type(debug).__name__)

print(config.getboolean('cache', 'enabled'))
print(server['PORT'], type(server['port']).__name__)

print(server.getint('max_clients', fallback=100))
print(server.get('host', 'localhost'))

try:
    config.getint('server', 'debug')
except ValueError as e:
    print(e)

We read the values with the proper types.

debug = server.getboolean('debug')
...
print(config.getboolean('cache', 'enabled'))

The getboolean method accepts 1/0, yes/no, true/false, and on/off, in any case.

print(server['PORT'], type(server['port']).__name__)

Keys are case-insensitive; Port, port, and PORT are the same key. Plain indexing returns a string.

print(server.getint('max_clients', fallback=100))
print(server.get('host', 'localhost'))

The fallback value is returned when the option is missing.

try:
    config.getint('server', 'debug')
except ValueError as e:
    print(e)

A value that cannot be converted raises a ValueError.

$ python typed_values.py
8080 int
2.5 float
True bool
False
8080 str
100
localhost
invalid literal for int() with base 10: 'yes'

The DEFAULT section

The special DEFAULT section provides values for all other sections.

defaults.ini
[DEFAULT]
host = localhost
timeout = 30

[mysql]
user = user7

[postgresql]
host = db.example.com
user = user8

Both sections inherit host and timeout; the postgresql section overrides host.

default_section.py
import configparser

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

print(config.sections())

for name in config.sections():
    sec = config[name]
    print(f"{name}: {sec['user']}@{sec['host']} timeout={sec['timeout']}")

print(dict(config.defaults()))

We print the effective values of each section.

print(config.sections())

The DEFAULT section is not listed among the sections.

print(dict(config.defaults()))

The defaults method returns the default values.

$ python default_section.py
['mysql', 'postgresql']
mysql: user7@localhost timeout=30
postgresql: user8@db.example.com timeout=30
{'host': 'localhost', 'timeout': '30'}

Reading strings and dictionaries

The read_string and read_dict methods load configuration from a string and from a dictionary.

read_from_string.py
import configparser

cfg_data = '''
[mysql]
host = localhost
user = user7
db = ydb
'''

config = configparser.ConfigParser()
config.read_string(cfg_data)
print(dict(config['mysql']))

config.read_dict({
    'mysql': {'port': 3306},
    'redis': {'host': 'localhost', 'port': 6379},
})

print(config.sections())
print(dict(config['mysql']))
print(config['redis']['port'], type(config['redis']['port']).__name__)

We combine both sources in one parser.

config.read_dict({
    'mysql': {'port': 3306},
    'redis': {'host': 'localhost', 'port': 6379},
})

Keys of the outer dictionary are section names; the inner dictionaries hold the options. New options are added to existing sections. Values are converted to strings, so the port of Redis comes back as '6379'.

$ python read_from_string.py
{'host': 'localhost', 'user': 'user7', 'db': 'ydb'}
['mysql', 'redis']
{'host': 'localhost', 'user': 'user7', 'db': 'ydb', 'port': '3306'}
6379 str

Writing configuration

The write method writes the configuration to a file.

writing.py
import configparser

config = configparser.ConfigParser()

config['mysql'] = {'host': 'localhost', 'user': 'user7', 'db': 'ydb'}

config.add_section('redis')
config.set('redis', 'host', 'localhost')
config['redis']['port'] = '6379'

config['mysql']['port'] = '3306'
config.remove_option('mysql', 'db')

with open('generated.ini', 'w') as f:
    config.write(f)

We create a configuration in code and save it.

config['mysql'] = {'host': 'localhost', 'user': 'user7', 'db': 'ydb'}

Assigning a dictionary creates a whole section.

config.add_section('redis')
config.set('redis', 'host', 'localhost')
config['redis']['port'] = '6379'

Alternatively, we add an empty section and set the options one by one. The set method and item assignment require string values.

config.remove_option('mysql', 'db')

Options and sections are deleted with remove_option and remove_section. Note that comments in a file are not preserved when the configuration is read and written back.

$ python writing.py
$ cat generated.ini
[mysql]
host = localhost
user = user7
port = 3306

[redis]
host = localhost
port = 6379

Interpolation

By default, ConfigParser uses basic interpolation: a value can refer to other options of the same section (or the DEFAULT section) with the %(name)s syntax.

cfg.ini
[info]
users_dir = C:\Users
name = Jano
home_dir = %(users_dir)s\%(name)s

We build the home_dir value from two other options. The trailing s is part of the syntax.

interpolation.py
import configparser

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

info = config['info']

print(info['users_dir'])
print(info['name'])
print(info['home_dir'])
print(info.get('home_dir', raw=True))

The example reads the values and prints them.

print(info.get('home_dir', raw=True))

With raw=True, we get the value without interpolation.

$ python interpolation.py
C:\Users
Jano
C:\Users\Jano
%(users_dir)s\%(name)s

The percent sign

With basic interpolation, a literal percent sign must be written as %%. A single % causes an error when the value is read.

percent.py
import configparser

data = '''
[report]
title = Sales up 50%
note = 100%% done
'''

config = configparser.ConfigParser()
config.read_string(data)

try:
    print(config['report']['title'])
except configparser.InterpolationSyntaxError as e:
    print(e)

print(config['report']['note'])

raw = configparser.ConfigParser(interpolation=None)
raw.read_string(data)
print(raw['report']['title'])
print(raw['report']['note'])

We read values with a percent sign.

try:
    print(config['report']['title'])
except configparser.InterpolationSyntaxError as e:
    print(e)

The value Sales up 50% raises an InterpolationSyntaxError.

raw = configparser.ConfigParser(interpolation=None)

If a file never uses interpolation, it is simplest to turn it off. Then every value is returned exactly as written, including %%.

$ python percent.py
'%' must be followed by '%' or '(', found: '%'
100% done
Sales up 50%
100%% done

Extended interpolation

ExtendedInterpolation uses the ${name} syntax and can refer to options in other sections with ${section:name}.

paths.ini
[common]
base = /opt/myapp

[paths]
logs = ${common:base}/logs
data = ${common:base}/data
archive = ${data}/archive

The paths are built from a base directory defined in another section.

extended.py
import configparser

config = configparser.ConfigParser(
    interpolation=configparser.ExtendedInterpolation())
config.read('paths.ini')

for key, value in config['paths'].items():
    print(f'{key:8} {value}')

We pass an ExtendedInterpolation instance to the parser.

archive = ${data}/archive

References are resolved recursively; archive refers to data, which refers to common:base.

$ python extended.py
logs     /opt/myapp/logs
data     /opt/myapp/data
archive  /opt/myapp/data/archive

Special values

Several constructor options relax the format: keys without values, values spanning several lines, inline comments, and options before the first section.

features.ini
version = 2

[mysqld]
user = mysql
skip-networking
# comment line
hosts =
    web1.example.com
    web2.example.com
    web3.example.com
pool = 10  ; inline comment

The file looks like a MySQL configuration file.

special.py
import configparser

config = configparser.ConfigParser(
    allow_no_value=True,
    allow_unnamed_section=True,
    inline_comment_prefixes=(';',),
)
config.read('features.ini')

print(config.sections())
print(config[configparser.UNNAMED_SECTION]['version'])

mysqld = config['mysqld']

print(mysqld['skip-networking'])
print('skip-networking' in mysqld)
print(mysqld['hosts'].split())
print(repr(mysqld['pool']))

We enable the options needed to read the file.

config = configparser.ConfigParser(
    allow_no_value=True,
    allow_unnamed_section=True,
    inline_comment_prefixes=(';',),
)

The allow_no_value option permits keys such as skip-networking; their value is None. Python 3.13 added allow_unnamed_section for options before the first section header; they are available under configparser.UNNAMED_SECTION. Inline comments are disabled by default, because ; may be part of a value.

print(mysqld['hosts'].split())

Indented lines continue the previous value. The value of hosts contains newlines, which we split into a list.

$ python special.py
[<UNNAMED_SECTION>, 'mysqld']
2
None
True
['web1.example.com', 'web2.example.com', 'web3.example.com']
'10'

Custom converters

The converters parameter adds custom get* methods.

converters.py
import configparser

data = '''
[web]
origins = localhost, example.com, test.example.com
ports = 80, 443, 8080
'''

def to_list(value):
    return [item.strip() for item in value.split(',')]

config = configparser.ConfigParser(converters={'list': to_list})
config.read_string(data)

web = config['web']

print(web.getlist('origins'))
print([int(p) for p in config.getlist('web', 'ports')])

We add a getlist method for comma-separated values.

config = configparser.ConfigParser(converters={'list': to_list})

The key list creates the getlist method on both the parser and its sections.

$ python converters.py
['localhost', 'example.com', 'test.example.com']
[80, 443, 8080]

Layered configuration

The read method accepts a list of files. Each file overrides the values of the previous ones, which gives an easy way to combine defaults with local settings.

base.ini
[server]
host = 0.0.0.0
port = 8080
debug = no

The base configuration is shipped with the application.

local.ini
[server]
debug = yes

The local file overrides only the debug option.

layered.py
import configparser

config = configparser.ConfigParser()
loaded = config.read(['base.ini', 'local.ini', 'missing.ini'])

print(loaded)
print(dict(config['server']))

We read the base file, the local file, and a file that does not exist.

loaded = config.read(['base.ini', 'local.ini', 'missing.ini'])

Missing files are skipped, so optional configuration files need no special handling.

$ python layered.py
['base.ini', 'local.ini']
{'host': '0.0.0.0', 'port': '8080', 'debug': 'yes'}

Source

Python configparser documentation

In this article we have used ConfigParser to work with configuration data in Python.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Python tutorials.