Ana içeriğe geç
Kesit Python
Orta · Standart kütüphane

Yapılandırma dosyaları: JSON, YAML, TOML, INI ve ortam değişkenleri

Ayarları koddan ayırır; JSON, YAML, TOML, INI ve .env dosyalarını okur, dev/test/prod ortamlarını katmanlı birleştirir, şemayı doğrular ve sırları koda gömmeden 12-factor ilkeleriyle yönetirsin.

Uygulamada aç, 18 alıştırmayı çözÜcretsiz, hesap gerekmez, kurulum yok.

Bu sayfa dersin okuma sürümüdür. Metin bir kutuyu çalıştırmanı ya da adıma geçmeni söylüyorsa dersi uygulamada aç.

Neden önemli?

Ilgaz Bank'ın kredi başvuru servisi üç yerde çalışıyor: geliştiricinin bilgisayarında (dev), test sunucusunda (test) ve gerçek müşterilere hizmet veren canlı ortamda (prod). Kod aynı; değişen şeyler veritabanı adresi, günlük kredi limiti, log seviyesi, e-posta gönderilip gönderilmeyeceği ve API anahtarları. Bunları kodun içine yazarsan her ortam için kodu değiştirmen, yeniden paketlemen gerekir; daha kötüsü, prod parolası bir gün GitHub'a gider.

Yapılandırma (configuration, kısaca config), kod değişmeden programın davranışını değiştiren her şeydir. Bu derste yaygın beş biçimi okumayı, ortamları katmanlı birleştirmeyi, ayarı doğrulamayı ve sırları güvenle yönetmeyi öğreneceksin. JSON'un temelini JSON ve CSV dersinde, tomllib'e ilk bakışı Sanal ortam (virtual environment) ve paket yönetimi dersinde gördün; burada hepsini yapılandırma gözüyle birleştiriyoruz.

Biçimleri tanı

BiçimOkumaYorumTiplerNerede görürsün?
JSONjson (standart)Yokstr, sayı, bool, null, liste, nesneAPI'ler, frontend ayarları
YAMLyaml.safe_load (pyyaml)#JSON + tarih, çok satırlı metin, referansDocker Compose, Kubernetes, GitHub Actions
TOMLtomllib (3.11+, yalnız okuma)#Açık tipler, tarih-saatpyproject.toml, Rust/Cargo
INIconfigparser (standart)# ;Hepsi metin; getint/getboolean ile çevirirsinEski uygulamalar, setup.cfg, Windows
.envos.environ + python-dotenv#Hepsi metinSırlar, ortama özel değerler

Kural: insanın düzenleyeceği ayar dosyası için TOML ya da YAML, makinelerin alışverişi için JSON, sırlar ve ortama özgü küçük değerler için ortam değişkeni (environment variable).

YAML: okunaklı ama tuzaklı

YAML girinti ile yapı kurar, yorum destekler ve çok okunaklıdır. Python'da üçüncü parti pyyaml ile okursun (pip install pyyaml; bu uygulamada hazır). Her zaman yaml.safe_load kullan: yaml.load uygun olmayan bir yükleyiciyle dosyadaki etiketlerden (tag) Python nesnesi oluşturabilir, bu da güvenilmeyen dosyada kod çalıştırma açığı demektir.

import yaml
metin = """
uygulama: kredi-servisi
surum: "1.4"
veritabani:
  host: db.ilgazbank.local
  port: 5432
subeler: [Ankara, İzmir, Bursa]
eposta_gonder: true
"""
ayar = yaml.safe_load(metin)
print(ayar["veritabani"]["port"] + 1, type(ayar["eposta_gonder"]).__name__)
print(ayar["subeler"][1])
print(yaml.dump(ayar, allow_unicode=True, sort_keys=False))

YAML 1.1 kuralları (pyyaml bunları uygular) bazı değerleri sessizce başka tipe çevirir. Bu tuzaklar gerçek projelerde hataya yol açar:

import yaml
print(yaml.safe_load("ulke: NO\naktif: no\nevet: yes"))   # NO ve no -> False!
print(yaml.safe_load("surum: 3.10"))                       # 3.1 (float)
print(yaml.safe_load("saat: 12:30"))                       # 750 (60'lık taban!)
print(yaml.safe_load("kod: 010"))                          # 8 (sekizlik taban)
print(yaml.safe_load('surum: "3.10"\nkod: "010"'))         # tırnakla metin kalır
Şüpheli her değeri tırnaklaÜlke kodu, sürüm numarası, başında sıfır olan kodlar, saat ve "yes/no/on/off" gibi kelimeler YAML'da tırnak içine yazılmalı. Bu sorun o kadar yaygındır ki adı bile vardır: "Norveç problemi" (Norway problem).

TOML: açık tipler

TOML, Python 3.11'den beri standart kütüphanededir (tomllib). Tipleri açıktır: metin her zaman tırnaklıdır, sayı ve tarih kendi tipindedir; YAML'daki sürprizler yoktur. tomllib yalnızca okur; yazmak için tomli-w paketi gerekir. Dosyadan okurken tomllib.load ikili modda açılmış dosya ister.

import tomllib
metin = """
[uygulama]
ad = "kredi-servisi"
baslangic = 2026-01-15

[veritabani]
host = "db.ilgazbank.local"
port = 5432

[[sube]]
ad = "Kızılay"
sehir = "Ankara"

[[sube]]
ad = "Alsancak"
sehir = "İzmir"
"""
ayar = tomllib.loads(metin)
print(ayar["uygulama"]["baslangic"].year, ayar["veritabani"]["port"])
print([s["ad"] for s in ayar["sube"]])
try:
    tomllib.loads("port = 5432\nport = 5433")
except tomllib.TOMLDecodeError as e:
    print("Hatalı TOML:", e)

[[sube]] bir tablo dizisi (array of tables) tanımlar; her blok listeye bir sözlük ekler. Aynı anahtarın iki kez yazılması TOML'da hatadır; YAML ise sessizce sonuncuyu alır.

INI: configparser

INI dosyası bölümlerden ([bolum]) ve anahtar = değer satırlarından oluşur. configparser her değeri metin olarak saklar; tipi sen istersin: getint, getfloat, getboolean. getboolean 1/yes/true/on ve 0/no/false/off değerlerini tanır. [DEFAULT] bölümündeki anahtarlar her bölüme miras kalır; anahtar adları büyük-küçük harf duyarsızdır.

import configparser
metin = """
[DEFAULT]
zaman_asimi = 30

[veritabani]
Host = localhost
port = 5432
debug = yes

[odeme_api]
url = https://odeme.example/v1
zaman_asimi = 5
"""
c = configparser.ConfigParser()
c.read_string(metin)
db = c["veritabani"]
print(db["host"], type(db["port"]).__name__, db.getint("port"))
print(db.getboolean("debug"), db.getint("zaman_asimi"))
print(c.getint("odeme_api", "zaman_asimi"), c.getint("odeme_api", "tekrar", fallback=3))
print(c.sections())

fallback eksik anahtar için varsayılan değer verir; vermezsen c.getint(bolum, anahtar) NoOptionError verir. [DEFAULT] bölümü sections() listesinde görünmez.

Bölüm nesnesi ile parser farkıc["veritabani"].getint("yok") eksik anahtarda hata vermez, sessizce None döndürür. Zorunlu bir alanı bu yolla okursan hata ancak None + 1 gibi bir işlemde, çok sonra çıkar. Zorunlu alanlar için c.getint("veritabani", "port") ya da c["veritabani"]["port"] (KeyError verir) kullan.

Ortam değişkenleri ve .env

İşletim sistemi her sürece bir ortam değişkenleri sözlüğü verir; Python'da os.environ'dur. Değerler her zaman metindir. Okurken os.environ["AD"] eksikse KeyError verir (zorunlu ayar için doğrusu bu), os.environ.get("AD", varsayilan) ise varsayılana düşer.

import os
os.environ["ANADOLU_PORT"] = "8080"
os.environ["ANADOLU_DEBUG"] = "false"
port = int(os.environ.get("ANADOLU_PORT", "8000"))
yanlis_debug = bool(os.environ["ANADOLU_DEBUG"])          # boş olmayan metin -> True!
dogru_debug = os.environ["ANADOLU_DEBUG"].lower() in {"1", "true", "yes", "on", "evet"}
print(port + 1, yanlis_debug, dogru_debug)
print(os.environ.get("ANADOLU_YOK", "varsayılan"))
try:
    os.environ["ANADOLU_DB_PAROLA"]
except KeyError as e:
    print("Zorunlu ayar eksik:", e)

Geliştirme sırasında onlarca değişkeni elle tanımlamak yerine proje kökünde bir .env dosyası tutulur ve python-dotenv paketi onu os.environ'a yükler:

# .env  (DEPOYA GİRMEZ, .gitignore'da)
APP_ENV=dev
ANADOLU_DB_URL=postgresql://localhost:5432/kredi
ANADOLU_DB_PAROLA="gizli parola"

# uygulama.py
from dotenv import load_dotenv
import os
load_dotenv()                      # .env'i okur, var olan değişkenleri EZMEZ
print(os.environ["APP_ENV"])
Kendi bilgisayarında denepython-dotenv tarayıcıda yüklü değildir: pip install python-dotenv. Mantığı basittir; alıştırmalarda kendi .env ayrıştırıcını yazacaksın. load_dotenv varsayılan olarak var olan ortam değişkenini değiştirmez; böylece sunucuda tanımlanan gerçek değer, unutulmuş bir .env dosyasından üstün gelir.

Ortama göre ayar: katmanlı birleştirme

Ölçeklenen projelerde ayar katmanlardan oluşur; sonraki katman öncekinin üzerine yazar:

  1. Koddaki güvenli varsayılanlar (her ortamda ortak).
  2. Ortak ayar dosyası (ayar.yaml).
  3. Ortama özel bölüm ya da dosya (ayar.prod.yaml), APP_ENV değişkeniyle seçilir.
  4. Ortam değişkenleri (sırlar ve son dakika değişiklikleri).
  5. Gerekirse komut satırı argümanları.

Sözlükleri {**a, **b} ya da a | b ile birleştirmek yüzeyseldir (shallow): iç içe bir sözlük komple değiştirilir. Prod dosyası yalnız veritabani.host değerini verdiyse port kaybolur. Bu yüzden iç içe (deep) birleştirme yazılır:

import copy
def birlestir(temel, ust):
    sonuc = copy.deepcopy(temel)
    for anahtar, deger in ust.items():
        if isinstance(deger, dict) and isinstance(sonuc.get(anahtar), dict):
            sonuc[anahtar] = birlestir(sonuc[anahtar], deger)
        else:
            sonuc[anahtar] = copy.deepcopy(deger)
    return sonuc

varsayilan = {"veritabani": {"host": "localhost", "port": 5432}, "log": "DEBUG", "limit": 50_000}
ortamlar = {
    "test": {"veritabani": {"host": "test-db"}},
    "prod": {"veritabani": {"host": "prod-db"}, "log": "WARNING"},
}
print(varsayilan | ortamlar["prod"])          # yüzeysel: port kayboldu
for ad in ["dev", "test", "prod"]:
    print(ad, birlestir(varsayilan, ortamlar.get(ad, {})))
print(varsayilan["veritabani"]["host"])         # varsayılan bozulmadı

Şema doğrulama: hızlı ve yüksek sesle hata ver

Yanlış yazılmış bir ayar (port yerine "5432a", limit yerine eksi sayı) programın başlangıcında yakalanmalı; gece 3'te ilk kredi başvurusunda değil. Bu ilkeye hızlı hata (fail fast) denir. Büyük projelerde pydantic ya da JSON Schema kullanılır; mantığı anlamak için elle yazmak öğreticidir. İyi bir doğrulayıcı ilk hatada durmaz, tüm hataları toplayıp birlikte söyler.

SEMA = {"host": str, "port": int, "debug": bool, "limit": float}

def dogrula(ayar):
    hatalar = []
    for anahtar, tur in SEMA.items():
        if anahtar not in ayar:
            hatalar.append(f"eksik: {anahtar}")
            continue
        deger = ayar[anahtar]
        if tur is float and isinstance(deger, int) and not isinstance(deger, bool):
            deger = float(deger)                     # 50000 da float sayılır
        if not isinstance(deger, tur) or (tur is int and isinstance(deger, bool)):
            hatalar.append(f"tür: {anahtar} {tur.__name__} olmalı")
    if isinstance(ayar.get("port"), int) and not 1 <= ayar["port"] <= 65535:
        hatalar.append("aralık: port 1-65535 olmalı")
    return hatalar

print(dogrula({"host": "db", "port": 5432, "debug": False, "limit": 50000}))
print(dogrula({"host": "db", "port": True, "limit": "çok"}))
print(dogrula({"host": "db", "port": 70000, "debug": True, "limit": 1.5}))

bool, int'in alt sınıfıdır: isinstance(True, int) True döner. Port için True kabul etmemek adına bu durumu ayrıca eledik. Doğrulanan ayarı bir @dataclass(frozen=True) nesnesine çevirirsen program çalışırken kimse yanlışlıkla ayarı değiştiremez ve editörün otomatik tamamlama yapar.

Sırlar ve 12-factor

Parola, API anahtarı, token gibi sırlar (secrets) asla koda, depoya giren bir dosyaya ya da loga yazılmaz. Depoya yalnızca değerleri boş bir .env.example girer; gerçek .env .gitignore'dadır. Canlı ortamda sırlar bir sır yöneticisinden (Vault, AWS Secrets Manager, Azure Key Vault) ya da dağıtım platformunun ortam değişkenlerinden gelir. Bir sır bir kez depoya girdiyse, silmek yetmez: geçmişte durur; o sırrı değiştirmen (rotate) gerekir.

12-factor app, bulutta çalışan uygulamalar için yaygın kabul görmüş bir ilkeler listesidir. Yapılandırmayla ilgili olanlar:

İlkeNe der?Pratikte
III. ConfigOrtama göre değişen her şey ortamda tutulur, kodda değilos.environ; kod depoyu açık kaynak yapsan bile sır sızmaz
IV. Backing servicesVeritabanı, kuyruk gibi servisler adresle bağlanan kaynaklardırDATABASE_URL değiştirerek kod değişmeden başka veritabanına geçersin
V. Build, release, runDerleme çıktısı + config = sürümAynı paket dev, test ve prod'a gider; yalnız config değişir
X. Dev/prod parityOrtamlar birbirine benzesinDev'de SQLite, prod'da PostgreSQL farkı sürpriz hata doğurur
def maskele(deger, gorunur=4):
    deger = str(deger)
    return "*" * max(len(deger) - gorunur, 3) + deger[-gorunur:] if len(deger) > gorunur else "***"

ayar = {"db_url": "postgresql://kredi@prod-db/kredi", "api_token": "tok_9f8e7d6c5b4a", "parola": "123"}
for anahtar, deger in ayar.items():
    gizli = any(k in anahtar for k in ("token", "parola", "secret", "key"))
    print(anahtar, "=", maskele(deger) if gizli else deger)
Gerçek işteEkiplerde en sık görülen olay raporlarından biri, bir geliştiricinin test için koda yazdığı anahtarın açık depoya itilmesidir; otomatik tarayıcı botlar dakikalar içinde bulur. Bu yüzden şirketler depoya gitleaks, detect-secrets gibi tarayıcılar ve pre-commit kancaları ekler. Kod incelemesinde (code review) "bu değer nereden geliyor, prod'da ne olacak?" sorusu standarttır; başlangıçta ayarı doğrulayan ve eksik değişkenin adını açıkça söyleyen bir yükleyici, dağıtım gecesini kurtarır.
Uzmanlaş

İç içe ayarlar için ortam değişkeni adlandırma

Ortam değişkenleri düzdür; iç içe yapıyı ad içinde kodlarsın: ANADOLU_VERITABANI__HOST=prod-db. Önek (ANADOLU_) başka uygulamaların değişkenleriyle çakışmayı, çift alt çizgi (__) iç içe anahtarı gösterir. pydantic-settings ve Dynaconf bu kuralı hazır uygular; Meydan okuma alıştırmasında kendin yazacaksın.

Tipi varsayılandan çıkarmak

Ortamdan gelen her şey metin olduğu için, değeri varsayılan ayardaki tipe göre çevirmek pratik bir yoldur: varsayılan int ise int(...), bool ise evet/hayır tablosu. Bilinmeyen bir anahtar gelirse sessizce yok saymak yerine hata vermek, ANADOLU_VERITABNI__HOST gibi yazım hatalarını yakalar.

Değişmez ayar nesnesi

types.MappingProxyType(sozluk) salt okunur bir sözlük görünümü verir; @dataclass(frozen=True) ise alan atamasını engeller. Ayarı tek bir yerde yükleyip uygulamaya değişmez nesne olarak dağıtmak, "bu değeri kim değiştirdi" sorusunu ortadan kaldırır.

Özellik bayrakları

Yeni bir özelliği kodla birlikte kapalı yayına alıp yapılandırmayla açmak (feature flag), riskli değişiklikleri geri almayı kolaylaştırır. Bayrakların ömrü kısa tutulmalı; açıldıktan sonra kod temizlenmezse yapılandırma dosyası zamanla anlaşılmaz hâle gelir.

Alıştırmalara geç (18)