The main question is: **do you want your configuration file to be in some Turing complete language** (like Python is)? If you do want that, you might also consider embedding some *other* (Turing complete) scripting language like [Guile][1] or [Lua][2] (because there could be perceived as "simpler" to use, or to embed, than Python is; read the chapter on [Extending & Embedding Python][3]). I won't discuss that further (but notice that embedding a scripting language in your application is a *major* architectural choice, that you should consider very *early*; I really don't recommend making that choice later!), because other answers (e.g. b[y Amon][4]) discussed that in depth.

<sup>(a well known example of a program configurable thru "scripts" is the [GNU emacs][5] editor; so be aware that if you accept script, some user would eventually use -and perhaps abuse, in your point of view- that facility extensively and make a multi-thousand lines script; hence the choice of a good enough scripting language is important)</sup>

However (at least on POSIX systems), you might consider convenient to enable the **configuration "file" to be dynamically *computed* at initialization time** (of course, leaving the burden of a sane configuration to your system admin or user; actually it is a configuration *text* which comes from some file or from some command). For that, you could simply adopt the *convention* (and *document* it) that a configuration file path starting with e.g. a `!` or a `|` is actually a shell *command* that you would read as a [pipeline][6]. This leaves your user with the choice of using whatever "preprocessor" or "scripting language" he is the most familiar with.

<sup>(you need to trust your user about security issues if you accept a dynamically computed configuration)</sup>

So in your initialization code, your `main` would (for example) accept some `--config` [argument][7] `confarg` and get some `FILE*configf;` from it. If that argument starts with `!` (i.e. if `(confarg[0]=='!')` ....), you would use `configf = popen(confarg+1, "r");` and close that pipe with `pclose(configf);`. Otherwise you would use `configf=fopen(confarg, "r");` and close that file with `fclose(configf);` (don't forget the error checking). See [pipe(7)][8], [popen(3)][9], [fopen(3)][10].

<sup>(document also for the weird user wanting to pass a configuration file named `!foo.config` to pass `./!foo.config` to bypass the `popen` trick above)</sup>

BTW, such a trick is only a convenience. If the user want to report any bug, he should send you the *generated* configuration file...


  [1]: https://www.gnu.org/software/guile/
  [2]: http://lua.org/
  [3]: https://docs.python.org/3/extending/index.html
  [4]: https://softwareengineering.stackexchange.com/a/351128/40065
  [5]: https://www.gnu.org/software/emacs/
  [6]: https://en.wikipedia.org/wiki/Pipeline_(Unix)
  [7]: https://softwareengineering.stackexchange.com/a/307472/40065
  [8]: http://man7.org/linux/man-pages/man7/pipe.7.html
  [9]: http://man7.org/linux/man-pages/man7/popen.7.html
  [10]: http://man7.org/linux/man-pages/man3/fopen.3.html