Config File Formats in Python: A Practical Guide

 · Gifted

Every Python project needs configuration at some point. Database credentials, feature flags, API keys, logging levels — something has to live outside your code. The question is: what format do you put it in?

There's no single right answer. There are, however, clear trade-offs. Here's what I've learned from using all of them.

The Contenders

Python projects typically use one of these formats:

Format Standard Library Support Human-Friendly Comments Data Types
INI / CFG configparser (built-in) Yes Yes Strings only
JSON json (built-in) Moderate No Full
YAML PyYAML (third-party) Very Yes Full
TOML tomllib (3.11+ built-in) Very Yes Full
.env python-dotenv (third-party) Yes Yes Strings only
Python files Nothing needed Yes Yes Anything

Let's walk through each one.

INI: The Old Reliable

INI files have been around since MS-DOS. Python's configparser module handles them natively.

import configparser

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

db_host = config['database']['host']
db_port = config.getint('database', 'port')
# config.ini
[database]
host = localhost
port = 5432
name = myapp

[logging]
level = INFO
file = /var/log/myapp.log

When to use it: Legacy projects, simple key-value configs, or when you want zero dependencies. INI is readable, supports sections, and everyone understands it immediately.

When to skip it: Everything is a string. No native support for lists, booleans, or nested structures. If your config has any complexity, you'll end up writing parsing code on top of configparser.

JSON: The Universal Language

JSON is everywhere. Python's json module is built in.

import json

with open('config.json') as f:
    config = json.load(f)

api_key = config['api']['key']
debug = config['debug']
{
  "debug": true,
  "api": {
    "key": "abc123",
    "timeout": 30
  },
  "features": ["auth", "billing", "notifications"]
}

When to use it: Machine-generated configs, API responses, or when your config is produced/consumed by other systems. JSON is the lingua franca of web services.

When to skip it: No comments. Trailing commas cause errors. Humans editing JSON by hand will make mistakes. If people need to maintain the file directly, JSON fights you.

YAML: The Human-First Format

YAML is designed to be readable. It supports comments, anchors, aliases, and complex nesting.

import yaml  # pip install pyyaml

with open('config.yaml') as f:
    config = yaml.safe_load(f)

db = config['database']
print(db['host'], db['port'])
# config.yaml
database:
  host: localhost
  port: 5432
  credentials:
    username: admin
    password: ${DB_PASSWORD}  # substituted at runtime

logging:
  level: INFO
  handlers:
    - console
    - file

When to use it: Complex configs with deep nesting, or when non-developers need to edit the file. Docker Compose, Ansible, and GitHub Actions all use YAML for this reason.

When to skip it: PyYAML is a third-party dependency. YAML's flexibility is a double-edged sword — indentation errors, unexpected type coercion (yes becomes True), and the infamous Norway problem. Always use safe_load(), never load().

TOML: The Modern Default

TOML was created specifically as a better INI. Python 3.11+ includes tomllib in the standard library.

import tomllib

with open('config.toml', 'rb') as f:
    config = tomllib.load(f)

title = config['project']['title']
version = config['project']['version']
# config.toml
[project]
title = "My App"
version = "1.2.0"

[database]
host = "localhost"
port = 5432

[logging]
level = "INFO"

When to use it: New Python projects. pyproject.toml is already the standard for Python packaging, so your team already knows TOML. It supports comments, has clear syntax, and handles types properly.

When to skip it: If you need to support Python < 3.11, you'll need the tomli backport package. Also, deeply nested TOML gets verbose — the [a.b.c.d] header syntax doesn't scale as gracefully as YAML's indentation.

.env Files: Secrets and Environment

.env files aren't a config format so much as a convention. Each line is a key-value pair.

from dotenv import load_dotenv
import os

load_dotenv()  # loads .env into os.environ

db_url = os.environ['DATABASE_URL']
secret_key = os.environ['SECRET_KEY']
# .env
DATABASE_URL=postgresql://user:pass@localhost/myapp
SECRET_KEY=super-secret-value
DEBUG=true

When to use it: Environment-specific secrets and variables. The 12-factor app methodology recommends storing config in the environment, and .env files are the local development equivalent.

When to skip it: Everything is a string. No nesting, no lists, no structure. .env files work best for a flat set of variables, not complex configuration. And never commit them to version control.

Python Files: Just Use Code

Some projects skip config files entirely and use a Python module.

# config.py
DEBUG = True
DATABASE = {
    'host': 'localhost',
    'port': 5432,
    'name': 'myapp',
}
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
from config import DEBUG, DATABASE

When to use it: When your config needs logic, or when you want IDE autocompletion and type checking. Django's settings.py is the canonical example.

When to skip it: Now you're executing untrusted code. If anyone can modify the config file, they can run arbitrary Python. This also makes it harder to generate or validate config programmatically.

What I Actually Use

For new Python projects, my default stack is:

  • pyproject.toml — project metadata and tool configuration (already standard)
  • .env — secrets and environment-specific values (loaded via python-dotenv)
  • config.yaml or config.toml — application-level configuration that humans edit

I avoid INI unless I'm maintaining something old. I avoid JSON for anything humans write. I avoid Python config files unless the project already uses that pattern.

The Real Rule

The best config format is the one your team will actually maintain correctly. A perfectly structured YAML file that nobody wants to edit is worse than a simple .env file people understand.

Pick the simplest format that handles your actual requirements. Add complexity only when the config demands it — not because a format is popular.