127

I am using ConfigParser to read the runtime configuration of a script.

I would like to have the flexibility of not providing a section name (there are scripts which are simple enough; they don't need a 'section'). ConfigParser will throw a NoSectionError exception, and will not accept the file.

How can I make ConfigParser simply retrieve the (key, value) tuples of a config file without section names?

For instance:

key1=val1
key2:val2

I would rather not write to the config file.

1

9 Answers 9

71

You can do this in a single line of code.

(But tomllib is probably a better choice. See the bottom of my answer.)

In python 3, prepend a fake section header to your config file data, and pass it to read_string().

from configparser import ConfigParser

parser = ConfigParser()
with open("foo.conf") as stream:
    parser.read_string("[top]\n" + stream.read())  # This line does the trick.

You could also use itertools.chain() to simulate a section header for read_file(). This might be more memory-efficient than the above approach, which might be helpful if you have large config files in a constrained runtime environment.

from configparser import ConfigParser
from itertools import chain

parser = ConfigParser()
with open("foo.conf") as lines:
    lines = chain(("[top]",), lines)  # This line does the trick.
    parser.read_file(lines)

In python 2, prepend a fake section header to your config file data, wrap the result in a StringIO object, and pass it to readfp().

from ConfigParser import ConfigParser
from StringIO import StringIO

parser = ConfigParser()
with open("foo.conf") as stream:
    stream = StringIO("[top]\n" + stream.read())  # This line does the trick.
    parser.readfp(stream)

With any of these approaches, your config settings will be available in parser.items('top').

You could use StringIO in python 3 as well, perhaps for compatibility with both old and new python interpreters, but note that it now lives in the io package and readfp() is now deprecated.

Alternatively, you might consider using a TOML parser instead of ConfigParser. Python 3.11 added a parser to the standard library: tomllib

Sign up to request clarification or add additional context in comments.

11 Comments

In the itertools.chain() example, why are you using a tuple and not a list, like this: lines = chain(["[top]"], lines)? Besides the somewhat unusual one-element tuple syntax, which may be unfamilliar to some people, using a list is also 1 character shorter and looks neater without the (required) dangling comma IMHO.
I'm in the habit of using tuples for sequences that will never change, because they're more efficient, less prone to error, and better at conveying the intent than lists. If someone has trouble understanding python's tuple syntax or believes brevity is more important than clarity, they can use a list instead.
great workaround, but this seems like it ought to "just work" when you set the default_section option in the ConfigParser constructor.
@ʇsәɹoɈ so there is an interpretation of the meaning of 'default_section' whereby if there is no section in the file it should be presumed to be the default section, allowing the processing of that file with no section.
tomllib is new in python 3.11, but 3.13 finally added unnamed sections to configparser. docs.python.org/3.13/library/configparser.html#unnamed-sections
|
58

Alex Martelli provided a solution for using ConfigParser to parse .properties files (which are apparently section-less config files).

His solution is a file-like wrapper that will automagically insert a dummy section heading to satisfy ConfigParser's requirements.

2 Comments

+1 because that is exactly what I was about to suggest. Why add all the complexity when all you have to do is just add a section!
@jathanism: there are cases where you want to work with existing config/properties files, which are read by existing Java code and you don't know the risk of modifying those headers
53

Enlightened by this answer by jterrace, I come up with this solution:

  1. Read entire file into a string
  2. Prefix with a default section name
  3. Use StringIO to mimic a file-like object
ini_str = '[root]\n' + open(ini_path, 'r').read()
ini_fp = StringIO.StringIO(ini_str)
config = ConfigParser.RawConfigParser()
config.readfp(ini_fp)


EDIT for future googlers: As of Python 3.4+ readfp is deprecated, and StringIO is not needed anymore. Instead we can use read_string directly:

with open('config_file') as f:
    file_content = '[dummy_section]\n' + f.read()

config_parser = ConfigParser.RawConfigParser()
config_parser.read_string(file_content)

2 Comments

This also works wonders to parse a simple Makefile (with only aliases)! Here is a full script to substitute aliases by their full commands in Python, inspired by this answer.
anyone else getting ` configparser.DuplicateOptionError` ?
21

You can use the ConfigObj library to do that simply : http://www.voidspace.org.uk/python/configobj.html

Updated: Find latest code here.

If you are under Debian/Ubuntu, you can install this module using your package manager :

apt-get install python-configobj

An example of use:

from configobj import ConfigObj

config = ConfigObj('myConfigFile.ini')
config.get('key1') # You will get val1
config.get('key2') # You will get val2

1 Comment

where is the get() method documented? The method indeed works but I can't find it reading through here: configobj.readthedocs.io/en/latest/configobj.html (which is pointed from the GitHub repo: github.com/DiffSK/configobj). I was hoping to find an equivalent set() method
10

The easiest way to do this is to use python's CSV parser, in my opinion. Here's a read/write function demonstrating this approach as well as a test driver. This should work provided the values are not allowed to be multi-line. :)

import csv
import operator

def read_properties(filename):
    """ Reads a given properties file with each line of the format key=value.  Returns a dictionary containing the pairs.

    Keyword arguments:
        filename -- the name of the file to be read
    """
    result={ }
    with open(filename, "rb") as csvfile:
        reader = csv.reader(csvfile, delimiter='=', escapechar='\\', quoting=csv.QUOTE_NONE)
        for row in reader:
            if len(row) != 2:
                raise csv.Error("Too many fields on row with contents: "+str(row))
            result[row[0]] = row[1] 
    return result

def write_properties(filename,dictionary):
    """ Writes the provided dictionary in key-sorted order to a properties file with each line of the format key=value

    Keyword arguments:
        filename -- the name of the file to be written
        dictionary -- a dictionary containing the key/value pairs.
    """
    with open(filename, "wb") as csvfile:
        writer = csv.writer(csvfile, delimiter='=', escapechar='\\', quoting=csv.QUOTE_NONE)
        for key, value in sorted(dictionary.items(), key=operator.itemgetter(0)):
                writer.writerow([ key, value])

def main():
    data={
        "Hello": "5+5=10",
        "World": "Snausage",
        "Awesome": "Possum"
    }

    filename="test.properties"
    write_properties(filename,data)
    newdata=read_properties(filename)

    print "Read in: "
    print newdata
    print

    contents=""
    with open(filename, 'rb') as propfile:
        contents=propfile.read()
    print "File contents:"
    print contents

    print ["Failure!", "Success!"][data == newdata]
    return

if __name__ == '__main__': 
     main() 

1 Comment

+1 Clever use of the csv module to solve common ConfigParser complaint. Easily generalized more and made to be both Python 2 & 3 compatible.
6

Having ran into this problem myself, I wrote a complete wrapper to ConfigParser (the version in Python 2) that can read and write files without sections transparently, based on Alex Martelli's approach linked on the accepted answer. It should be a drop-in replacement to any usage of ConfigParser. Posting it in case anyone in need of that finds this page.

import ConfigParser
import StringIO

class SectionlessConfigParser(ConfigParser.RawConfigParser):
    """
    Extends ConfigParser to allow files without sections.

    This is done by wrapping read files and prepending them with a placeholder
    section, which defaults to '__config__'
    """

    def __init__(self, *args, **kwargs):
        default_section = kwargs.pop('default_section', None)
        ConfigParser.RawConfigParser.__init__(self, *args, **kwargs)

        self._default_section = None
        self.set_default_section(default_section or '__config__')

    def get_default_section(self):
        return self._default_section

    def set_default_section(self, section):
        self.add_section(section)

        # move all values from the previous default section to the new one
        try:
            default_section_items = self.items(self._default_section)
            self.remove_section(self._default_section)
        except ConfigParser.NoSectionError:
            pass
        else:
            for (key, value) in default_section_items:
                self.set(section, key, value)

        self._default_section = section

    def read(self, filenames):
        if isinstance(filenames, basestring):
            filenames = [filenames]

        read_ok = []
        for filename in filenames:
            try:
                with open(filename) as fp:
                    self.readfp(fp)
            except IOError:
                continue
            else:
                read_ok.append(filename)

        return read_ok

    def readfp(self, fp, *args, **kwargs):
        stream = StringIO()

        try:
            stream.name = fp.name
        except AttributeError:
            pass

        stream.write('[' + self._default_section + ']\n')
        stream.write(fp.read())
        stream.seek(0, 0)

        return ConfigParser.RawConfigParser.readfp(self, stream, *args,
                                                   **kwargs)

    def write(self, fp):
        # Write the items from the default section manually and then remove them
        # from the data. They'll be re-added later.
        try:
            default_section_items = self.items(self._default_section)
            self.remove_section(self._default_section)

            for (key, value) in default_section_items:
                fp.write("{0} = {1}\n".format(key, value))

            fp.write("\n")
        except ConfigParser.NoSectionError:
            pass

        ConfigParser.RawConfigParser.write(self, fp)

        self.add_section(self._default_section)
        for (key, value) in default_section_items:
            self.set(self._default_section, key, value)

Comments

5

Blueicefield's answer mentioned configobj, but the original lib only supports Python 2. It now has a Python 3+ compatible port:

https://github.com/DiffSK/configobj

APIs haven't changed, see it's doc.

2 Comments

While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - From Review
And what if the API change? You can always edit answer if it's outdated, be it a link or code snippet. I've done it for numerous times, I don't see why you think it's a problem.
2

Python 3.13 finally support files without sections.

https://docs.python.org/3.13/library/configparser.html#unnamed-sections

1 Comment

hey thanks for posting this. I discovered unnamed sections too and works like a charm! I wanted to add sample code for peeps new to this feature so I wasn't sure if the protocol here is to edit your answer or post or new one (since your answer is technically "complete")
1

To build on Pedro's discovery of support for unnamed sections in Python 3.13, an example:

import configparser

configfile = "config.ini"

config = configparser.ConfigParser(allow_unnamed_section=True)
config.read(configfile)

value = config.get(configparser.UNNAMED_SECTION, 'key1')

and while OP has no need to write/modify the original file, for those who do:

config.set(configparser.UNNAMED_SECTION, 'key1', 'new_val')

with open(configfile, 'w') as cf:
    config.write(cf)

NOTE: config.optionxform=str is useful if you want to preserve case of your option names (aka. keys) (otherwise all option names get written back out in all lowercase which breaks programs expecting case-sensitivity) (see this answer)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.