Argumen CLI cocok untuk pengaturan yang berubah setiap eksekusi; environment variables cocok untuk perbedaan antar lingkungan. Untuk pengaturan yang terstruktur dan hidup lama — host database, level log, daftar folder yang dipantau — berkas konfigurasi adalah rumah yang tepat. Pada Chapter ini kita memuat berkas TOML, JSON, dan YAML, menggabungkannya dengan nilai default dan environment variables, lalu menemukan lokasi berkasnya mengikuti konvensi sistem operasi.

Tiga Format, Satu Bentuk Data #

Ketiga format berikut memuat isi yang identik. Berkas konfigurasi.toml:

toml
host = "example.com"

[log]
level = "DEBUG"

Berkas konfigurasi.json:

json
{
  "host": "example.com",
  "log": {
    "level": "DEBUG"
  }
}

Berkas konfigurasi.yaml:

yaml
host: example.com
log:
  level: DEBUG

Skrip baca_tiga_format.py memuat ketiganya lalu membandingkan hasilnya:

python
import json
import tomllib

import yaml

with open("konfigurasi.toml", "rb") as berkas:
    dari_toml = tomllib.load(berkas)

with open("konfigurasi.json") as berkas:
    dari_json = json.load(berkas)

with open("konfigurasi.yaml") as berkas:
    dari_yaml = yaml.safe_load(berkas)

print("isi TOML:", dari_toml)
print("sama semua:", dari_toml == dari_json == dari_yaml)

Output yang Dihasilkan:

text
isi TOML: {'host': 'example.com', 'log': {'level': 'DEBUG'}}
sama semua: True

Apapun formatnya, hasil akhirnya adalah dictionary bertingkat yang sama, sehingga logika program tidak perlu tahu asal konfigurasi. Perlu dicatat: tomllib hadir di Python 3.11 dan hanya membaca TOML; json adalah standard library; sedangkan YAML memerlukan paket pihak ketiga PyYAML. Selalu gunakan yaml.safe_load(), bukan yaml.load() — alasan keamanannya dibahas pada Chapter Deserialization Risks.

Perbandingan ketiganya dirangkum berikut:

AspekTOMLJSONYAML
Dukungan standard libraryya (tomllib, 3.11+)ya (json)tidak
Komentaryatidakya
String multibarisyalewat escape \nya
Tipe data bawaanangka, bool, tanggalterbataskaya, termasuk null
Kecenderungan galat penulisanrendahkurung dan koma yang telatindentasi

Konfigurasi Berlapis: defaults, Berkas, Environment #

Praktik umum aplikasi matang: simpan default dalam kode, timpa dengan berkas, lalu timpa lagi dengan environment variables. Penggabungan dictionary bertingkat memerlukan fungsi merge yang menurun ke dalam. Berkas config_berlapis.py:

python
import os
import tomllib


def deep_merge(base, override):
    hasil = dict(base)
    for kunci, nilai in override.items():
        if isinstance(nilai, dict) and isinstance(hasil.get(kunci), dict):
            hasil[kunci] = deep_merge(hasil[kunci], nilai)
        else:
            hasil[kunci] = nilai
    return hasil


DEFAULTS = {
    "host": "localhost",
    "port": 8000,
    "log": {"level": "INFO", "warna": False},
}

with open("konfigurasi.toml", "rb") as berkas:
    dari_berkas = tomllib.load(berkas)

env_override = {}
if "APP_PORT" in os.environ:
    env_override["port"] = int(os.environ["APP_PORT"])

config = deep_merge(DEFAULTS, dari_berkas)
config = deep_merge(config, env_override)

print("host:", config["host"])
print("port:", config["port"])
print("log :", config["log"])

Mari kita bedah inti mekanismenya:

  1. Baris 5-12 (def deep_merge): menyalin base agar tidak berubah, lalu menimpanya dengan pasangan kunci-nilai dari override.
  2. Baris 8-9 (isinstance(nilai, dict)): bila kedua nilai bertipe dictionary, fungsi memanggil dirinya sendiri sehingga satu cabang dapat menimpa sebagian saja.
  3. Baris 24-26 (env_override): environment variables diterjemahkan ke bentuk dictionary agar bisa melewati deep_merge yang sama.
  4. Baris 28-29 (config = deep_merge(DEFAULTS, dari_berkas)): urutan pemanggilan menentukan prioritas — berkas menimpa default, lalu environment menimpa berkas.

Output yang Dihasilkan tanpa APP_PORT:

text
host: example.com
port: 8000
log : {'level': 'DEBUG', 'warna': False}

Perhatikan bahwa log.warna tetap False dari default meski berkas hanya mengatur log.level — inilah manfaat merge bertingkat dibanding sekadar dict.update(). Dengan APP_PORT=9000, environment menimpa kedua lapisan sebelumnya:

text
host: example.com
port: 9000
log : {'level': 'DEBUG', 'warna': False}

Menemukan Lokasi Berkas Config #

Program perlu mencari berkas konfigurasi tanpa hardcode path absolut. Konvensi XDG pada Linux menempatkannya di ~/.config/<nama-app>/config.toml, dengan /etc/<nama-app>.toml sebagai lokasi sistem. Fungsi cari_config() berikut memeriksa kandidat dari yang paling spesifik:

python
from pathlib import Path


def cari_config(nama_app):
    kandidat = [
        Path.home() / ".config" / nama_app / "config.toml",
        Path("/etc") / f"{nama_app}.toml",
    ]
    for path in kandidat:
        if path.is_file():
            return path
    return kandidat[0]


lokasi = cari_config("aplikasi-saya")
print("berkas config :", lokasi.name)
print("folder config  :", lokasi.parent.name)

Output yang Dihasilkan (dengan ~/.config/aplikasi-saya/config.toml yang sudah ada):

text
berkas config : config.toml
folder config  : aplikasi-saya

Bila tidak ada kandidat yang ditemukan, fungsi mengembalikan kandidat pertama — path tempat program bisa membuat berkas baru. Berikan juga opsi eksplisit seperti --config path/ke/berkas agar pengguna tetap bisa memakai lokasi khusus.

Validasi Schema #

Berkas konfigurasi ditulis manusia, jadi salah ketik pasti terjadi: port = "delapan" atau host yang hilang akan meledak jadi galat aneh jauh di dalam program. Validasi schema menolak konfigurasi jelek sedini mungkin dengan pesan yang jelas. Library pydantic adalah pilihan populer untuk tugas ini:

python
from pydantic import BaseModel, ValidationError


class Config(BaseModel):
    host: str
    port: int


try:
    Config(host="example.com", port="bukan angka")
except ValidationError as galat:
    print("konfigurasi tidak valid:", galat.error_count(), "galat")

Output yang Dihasilkan:

text
konfigurasi tidak valid: 1 galat

Class Config mendefinisikan struktur beserta tipe tiap kolom; pydantic memeriksa data hasil parsing terhadap definisi tersebut. Alternatif yang lebih ringan adalah dataclass plus pemeriksaan manual, materi Chapter Dataclasses.

Dengan pemuatan berlapis, penemuan lokasi standar, dan validasi schema, rantai konfigurasi kita lengkap: default dalam kode, berkas untuk yang stabil, environment untuk yang berubah per lingkungan, dan argumen CLI untuk penimpaan ad hoc. Berikutnya kita keluar dari proses Python untuk menjalankan program lain melalui subprocess.