TEKNİK YAZI / Backend Sistemleri

Python 3.15 Yenilikleri: Lazy Import, UTF-8 Geçişi ve Yükseltmede Kırılanlar

Python 3.15 yenilikleri deneyle: Lazy import başlangıcı 172 ms'den 16 ms'ye indirdi, UTF-8 varsayılanı eski Türkçe dosyaları kırıyor, PyYAML'da wheel yok.

Python 3.15, 9 Ekim 2026’da yayımlanan ve lazy import söz dizimini, frozendict tipini, varsayılan UTF-8 kodlamasını getiren Python sürümüdür. Yükseltme kararı yenilik listesinden çok üç soruya bağlı: Başlangıç süresi gerçekten düşüyor mu, eski Türkçe dosyalara ne oluyor, bağımlılıklar hazır mı?

Bu yazı üç soruyu da deneyle yanıtlıyor. Python 3.15.2 ve 3.14.10’u 4 Kasım 2026’da macOS aarch64 üzerinde, yükü yüksek bir makinede denedim. Linux ve Windows’ta denemedim. Deneyle görmediğim her iddiayı kaynağına atfediyorum.

Kısaca

Python 3.15’te yükseltmeyi neler etkiliyor?

Aşağıdaki tablo yükseltme kararına dokunan değişiklikleri ve bu yazıdaki kanıt düzeyini gösteriyor. Tam liste What’s New sayfasında.

DeğişiklikKimi etkiliyorKanıt
lazy import (PEP 810)Başlangıcı ağır CLI ve kısa ömürlü işlerÖlçtüm
Varsayılan UTF-8 (PEP 686)encoding vermeyen kod, UTF-8 olmayan yerel ayarlarDenedim
Kaldırılan API’lerEski kod ve eski bağımlılıklarDenedim
frozendict (PEP 814)Önbellek anahtarları, değişmez yapılandırmaDenedim
Tachyon profilcisi (PEP 799)Canlı ortamda profil almak isteyen ekiplerBelge
JITCPU yoğun saf Python koduBelge

Sürüm tarafında iki bilgi önemli. PEP 790 takvimine göre düzenli düzeltme sürümleri Ekim 2028’e kadar yaklaşık iki ayda bir gelecek, güvenlik desteği yaklaşık Ekim 2031’de bitecek. 3.15.2 ise hızlandırılmış bir güvenlik sürümü: ssl modülü ele geçirilen eMudhra ara CA anahtarlarını engelliyor (CVE-2026-115557), tarfile filtresi hedefin yerine geçen bağlantıları reddediyor (CVE-2026-114583). Bu yüzden deneylerimi de 3.15.2 ile yaptım.

Lazy import nedir, nasıl çalışır?

Lazy import, modülü import satırında değil, adın ilk kullanıldığı anda yükleyen içe aktarma biçimidir. Python satıra geldiğinde ad alanına bir vekil nesne koyar. Ad ilk kullanıldığında modülü yükler ve vekili gerçek nesneyle değiştirir. Denemede globals()["wave"] önce lazy_import, ilk erişimden sonra module tipinde görünüyor.

Söz dizimi PEP 810 ile geldi. Ölçümde kullandığım küçük CLI şöyle:

import sys
lazy import httpx
lazy import pydantic
lazy from rich.console import Console


def main():
    if "--version" in sys.argv:
        print("cli 1.0")
        return
    Console().print(httpx.__version__, pydantic.VERSION)


main()
İki satırlı zaman çizelgesi. Normal import modülü import satırında yükler ve eksik modül hatası orada çıkar. Lazy import o satırda yalnız vekil koyar, modül ada ilk erişimde yüklenir ve hata orada çıkar. --version yolu ilk erişimden önce biter.
Şekil 1. Lazy import yükleme maliyetini ve eksik modül hatasını birlikte ilk erişime taşır.

Lazy import ne kadar kazandırıyor?

Aynı CLI’ı normal ve lazy import ile 31’er kez, durumları sırayla dönüşümlü çalıştırarak ölçtüm. Paketler httpx, pydantic ve rich. Her çalıştırma ayrı bir süreçti.

Yatay çubuk grafik. --version yolunda boş yorumlayıcı 16,1, normal import 172,5, lazy import 16,2, -X lazy_imports=all 15,3 milisaniye. Gerçek işte normal import 172,4, lazy import 176,8 milisaniye.
Şekil 2. Kazanç yalnız kullanılmayan modüllerde. Modüller kullanılınca iki çubuk aynı boyda.
DurumMedyanEn azEn çok
Boş yorumlayıcı16,1 ms14,3 ms33,1 ms
--version, normal import172,5 ms160,0 ms217,0 ms
--version, lazy import16,2 ms14,8 ms22,0 ms
--version, -X lazy_imports=all15,3 ms14,2 ms28,3 ms
Gerçek iş, normal import172,4 ms158,8 ms189,2 ms
Gerçek iş, lazy import176,8 ms159,6 ms249,7 ms

Makine ölçüm sırasında yüklüydü (yük ortalaması 24,5), bu yüzden mutlak değerlere değil orana bakın. Üç sonuç çıkıyor:

  1. Kullanılmayan iş tamamen kalkıyor. --version yolu boş yorumlayıcıyla aynı süreye indi. Yüklenen yeni modül 219’dan 13’e, en yüksek bellek 43,0 MB’tan 16,8 MB’a düştü.
  2. Kullanılan iş hızlanmıyor. Gerçek iş yolunda 172,4 ve 176,8 ms arasındaki fark gürültü sınırında, en küçük değerler 158,8 ve 159,6 ms. Bu yüzden ne kazanç ne de kayıp iddia ediyorum.
  3. Maliyet kaybolmuyor, yer değiştiriyor. Tek başına httpx adına ilk erişim 107,9 ms sürdü. Bir web sunucusunda bu süre başlangıçtan ilk isteğe taşınır. Uzun ömürlü bir serviste bu her zaman istenen bir sonuç değildir; bu yüzden kendi servisinizde ayrıca ölçün. Ben yalnız CLI ölçtüm.

Hangi modülün aday olduğunu görmek için -X importtime yeterli. İkinci sütun, modülün bağımlılıklarıyla birlikte toplam süresini mikrosaniye olarak verir:

python3.15 -X importtime -c "import httpx" 2>&1 | tail -n 3

Lazy import neleri bozabilir?

Eksik bağımlılık geç görünür. Aşağıdaki kod baslangic gecti yazar, hata ancak son satırda çıkar:

lazy import olmayan_modul

print("baslangic gecti")
olmayan_modul.basla()

İz kaydı hem lazy import satırını hem de kullanım satırını gösteriyor. Sorun şu: Nadiren çalışan bir kod yolu, eksik paketi günlerce saklayabilir. CI’da bütün modülleri gerçekten yükleyen bir adım bulundurun. Filtre False döndürdüğünde lazy işaretli import da hemen yükleniyor:

import sys

sys.set_lazy_imports_filter(lambda importing, imported, fromlist: False)

PEP metni ile sürüm ayrışıyor. PEP 810, none adında bir moddan söz ediyor. Python 3.15.2’de -X lazy_imports=none ve PYTHON_LAZY_IMPORTS=none yorumlayıcıyı başlangıçta ölümcül hatayla durduruyor, sys.set_lazy_imports("none") ise ValueError veriyor. Komut satırı belgesi yalnız all ve normal değerlerini listeliyor. Lazy davranışı kapatmak için yukarıdaki filtreyi kullanın.

try ve fonksiyon içinde kullanılamaz. İkisi de SyntaxError veriyor. try/except ImportError ile yazılmış isteğe bağlı bağımlılıklar olduğu gibi kalmalı.

Yan etkiler gecikir. Modül düzeyinde kayıt yapan bir eklenti ilk kullanıma kadar çalışmaz. Denemede ana calisti satırı eklenti yuklendi satırından önce yazıldı. Kayıt düzenine dayanan kodda modülleri açıkça yükleyen bir fonksiyon daha güvenlidir.

-X lazy_imports=all bütün süreci etkiler. PEP bu modu kütüphane yazarları için değil, uygulamayı işletenler için tanımlıyor. Ölçümde --version yolunda 15,3 ms verdi, ama import sırasına bağlı bir kütüphaneyi bozabilir. Sorunlu modülü filtreyle dışarıda bırakabilirsiniz:

import sys


def filtre(importing, imported, fromlist):
    return imported != "json"


sys.set_lazy_imports_filter(filtre)
sys.set_lazy_imports("all")

3.14 söz dizimini tanımaz. lazy import json 3.14’te SyntaxError veriyor. Birden fazla sürümü destekleyen kütüphane __lazy_modules__ listesini kullanabilir. Aşağıdaki kodda wave 3.15’te ilk kullanıma kadar yüklenmiyor, 3.14’te normal import gibi çalışıyor:

__lazy_modules__ = ["wave"]
import wave

UTF-8 varsayılanı Türkçe dosyaları nasıl etkiler?

PEP 686 ile encoding verilmeyen open() çağrıları sistem yerel ayarına değil UTF-8’e bakıyor. locale.getencoding() hâlâ ISO8859-9 döndürse de open() bunu kullanmıyor.

Denemede aynı yedi harfi (şığöçüİ) iki kodlamada dosyaya yazdım, iki sürümde ve iki yerel ayarda okudum.

Karşılaştırma tablosu. ISO-8859-9 yerel ayarında 3.14 eski dosyayı doğru, UTF-8 dosyayı sessizce bozuk okur; 3.15 eski dosyada ve metin kipindeki alt süreçte UnicodeDecodeError verir. UTF-8 yerel ayarında iki sürüm aynıdır. Açık encoding her durumda doğru okur.
Şekil 3. Davranış yalnız UTF-8 olmayan yerel ayarda değişiyor. En riskli hücre hata vermeyen hücre.

Üç şey önemli:

  1. UTF-8 yerel ayarında davranış değişmiyor. tr_TR.UTF-8 altında 3.14 ve 3.15 aynı sonuçları verdi. Güncel Linux ve macOS kurulumlarının çoğu bu gruptadır.
  2. ISO-8859-9 yerel ayarında eski dosya okunamaz hâle geliyor. open(), Path.read_text() ve subprocess.run(text=True) 3.15’te UnicodeDecodeError veriyor.
  3. Karışık ortamda sessiz olan yön daha tehlikeli. 3.15’in yazdığı UTF-8 dosyayı aynı yerel ayardaki 3.14 hata vermeden yanlış okuyor. Bir servis yükseltilmiş, öbürü yükseltilmemişse bozuk metin veritabanına kadar gidebilir.

Windows’ta denemedim. Bu yedi harf cp1254 ile ISO-8859-9’da aynı baytlara sahip, bu yüzden Türkçe Windows’ta aynı dosya için aynı hatayı beklerim. Bu bir çıkarım, ölçüm değil.

Çözüm, dosyanın kodlamasını kodda açıkça belirtmek:

from pathlib import Path

metin = Path("eski.txt").read_text(encoding="iso-8859-9")

Hangi kod Python 3.15’te kırılıyor?

İki grup var, çünkü yakalama yöntemleri farklı. Bu grupların her satırını iki sürümde çalıştırdım.

3.14’te uyarı veren, 3.15’te kırılan

Bu satırlar 3.14’te çalışıyor. -W error::DeprecationWarning verince 3.14’te de hata veriyor, yani yükseltmeden yakalanıyor.

Kod3.143.14, -W error3.15
strptime("15 Mar", "%d %b")1900-03-15YakalanıyorValueError
strptime(satir, "%b %d %H:%M:%S")1900-03-15 10:20:30YakalanıyorValueError
import sre_parseÇalışıyorYakalanıyorModuleNotFoundError
PurePath("a").is_reserved()ÇalışıyorYakalanıyorAttributeError
sqlite3.connect(":memory:", 5.0)ÇalışıyorYakalanıyorTypeError

En olası sürpriz yılsız tarih ayrıştırma. Syslog biçimindeki %b %d %H:%M:%S tam olarak bu durumda: 3.14 uyarıyla 1900 yılını varsayıyor, 3.15 ValueError veriyor. Çözüm, yılı açıkça eklemek:

from datetime import datetime

yil = 2026  # Günlüğün bağlamından gelmeli, örneğin dosya adından.
satir = "Mar 15 10:20:30"
zaman = datetime.strptime(f"{yil} {satir}", "%Y %b %d %H:%M:%S")

Bu grubu yükseltmeden yakalayabilirsiniz. Testleri 3.14’te, uyarıları hataya çevirerek çalıştırın:

python3.14 -X warn_default_encoding -W error::DeprecationWarning -W error::EncodingWarning -m pytest

Bağımlılıklardan gelen uyarılar da hata olur. Eski bir bağımlılık sre_parse kullanıyorsa onu burada görürsünüz.

Uyarı vermeden davranışı değişen

Kod3.143.15Düzeltme
add_argument("-f", "-foo").destffoodest değerini açık yaz
base64.urlsafe_b64decode(b"YWI")binascii.Errorb'ab'padded=True ver
hasattr(json, "__cached__")TrueFalse__spec__.cached kullan

-W error bunları yakalamıyor. argparse satırını uyarılar hataya çevrilmişken de denedim, 3.14 sessizce f döndürdü. base64 satırı ayrıca dikkat istiyor: Dolgu kontrolüne güvenerek girdi doğrulayan kod, 3.15’te eksik dolgulu girdiyi hatasız kabul eder. padded=True verince aynı girdi yine binascii.Error veriyor.

Bu grubu kod aramasıyla bulun:

grep -rnE "add_argument\(['\"]-[a-zA-Z]['\"], *['\"]-[a-zA-Z]{2,}|urlsafe_b64decode|__cached__" --include='*.py' .

Burada denediklerim tam liste değil. sqlite3 geri çağrı parametreleri, unittest.assertWarns ve başka kaldırmalar için What’s New sayfasındaki “Porting to Python 3.15” ve “Removed” bölümlerine bakın.

Bağımlılıklar Python 3.15’e hazır mı?

Önce PyPI dosya listesine baktım. 4 Kasım’da 20 yaygın paketin 19’unda 3.15’e uygun bir wheel dosyası vardı. Bu sayı iyimser çıktı. Aynı paketleri bağımlılık ağacıyla ve kaynaktan derlemeyi kapatarak çözünce kurulabilen sayı platforma göre 17 veya 18’e düştü. Liste benim seçimim, temsilî bir örneklem değil. Durum her gün değişebilir.

Dört hedefli tablo. On altı paket her hedefte wheel ile kuruluyor. orjson glibc 2.28 hedefinde glibc 2.39 istediği için kurulamıyor, uvloop Windows'ta wheel bulamıyor, tokenizers her hedefte pyyaml'a takılıyor, pyyaml'ın hiçbir hedefte wheel'i yok. Kurulabilen sayı macOS arm64'te 18, glibc 2.28'de 17, glibc 2.39'da 18, Windows'ta 17.
Şekil 4. Aynı 20 paket, kaynak kurulumu kapalıyken dört hedefte çözüldü. Sorun dört pakette toplanıyor.

Tabloda üç ayrı durum var:

  1. Wheel var diye her Linux’ta kurulmuyor. orjson 3.13.0’ın 3.15 Linux wheel’leri manylinux_2_39 etiketli, yani PEP 600 gereği en az glibc 2.39 istiyor. Aynı sürümün 3.14 wheel’leri manylinux_2_17 ile yetiniyor. Ubuntu 24.04 (2.39) ve Debian 13 (2.41) bu şartı karşılıyor, Ubuntu 22.04 (2.35) ve Debian 12 (2.36) karşılamıyor. Yani Python’u yükseltmek taban imajı da yükseltmeyi gerektirebilir.
  2. Eksik wheel bağımlılık zinciri boyunca yayılıyor. tokenizers paketinin kendi abi3 wheel’i var, ama huggingface-hub üzerinden PyYAML’a bağlı. Kaynak kurulumu kapalıyken kendi wheel’i olan paket de çözülmüyor.
  3. uvloop ayrı bir durum. Windows wheel’i 3.14’te de yok, yani bu 3.15’e özgü değil.

PyYAML kurulumu neden yanıltıcı?

Eksik wheel kurulumu durdurmuyor:

KurulumYükleyiciAynı belge
3.14, wheelC42,8 ms
3.14, wheelSaf Python295,4 ms
3.15, kaynaktan kurulumSaf Python288,3 ms

Yavaşlık Python sürümünden gelmiyor, C yükleyicisinin yokluğundan geliyor. PyYAML belgesindeki kalıp bu durumu saklıyor, çünkü ImportError hatasını yutup saf Python yükleyicisine geçiyor:

try:
    from yaml import CSafeLoader as Loader
except ImportError:
    from yaml import SafeLoader as Loader

Kaynaktan kurulumun sonucu makineye göre değişebilir. Ortak nokta, kurulumun başarılı görünmesi.

Kendi listenizi nasıl denersiniz?

Dosya listesine bakmak yetmediği için bağımlılık ağacını çözün ve kaynak kurulumunu kapatın. uv pip compile bunu 3.15 kurmadan ve paketleri kurmadan, seçtiğiniz platform için yapıyor:

uv pip compile requirements.txt --python-version 3.15 --python-platform x86_64-manylinux_2_28 --only-binary :all:

--python-platform değerine canlı ortamınızın platformunu yazın, örneğin x86_64-manylinux_2_39 veya x86_64-pc-windows-msvc. Komut ilk çözümsüz pakette durur. Hepsini bir arada görmek için paketleri tek tek deneyin:

while read -r paket; do
  echo "$paket" | uv pip compile - -q --python-version 3.15 --python-platform x86_64-manylinux_2_28 --only-binary :all: > /dev/null 2>&1 || echo "wheel ile kurulamıyor: $paket"
done < requirements.txt

Hata “wheel yok” demektir, “paket kırık” demek değildir. Saf Python olup yalnız kaynak dağıtımı yayımlayan bir paket de bu listeye düşer. Böyle bir paketi uv’de --no-binary <paket> ile muaf tutabilirsiniz, ama önce C uzantısı içermediğini doğrulayın.

macOS hedefinde torch için MACOSX_DEPLOYMENT_TARGET=14.0 vermem gerekti, çünkü wheel macOS 14 istiyor. uv kullanmıyorsanız aynı denetimi, yalnız çalıştığınız makine için, pip ile yapabilirsiniz. Şu komut denemede PyYAML için No matching distribution found verip 1 koduyla çıktı:

python3.15 -m pip install --dry-run --ignore-installed --only-binary :all: -r requirements.txt

frozendict ve küçük eklemeler ne işe yarıyor?

frozendict (PEP 814) değişmez sözlüktür. Bütün değerleri hashable ise kendisi de hashable olur, yani tuple ile aynı kuralı izler. En pratik kullanımı önbellek anahtarı:

from functools import lru_cache


@lru_cache
def baglanti_kur(ayar):
    return ayar["host"], ayar["port"]


print(baglanti_kur(frozendict(host="localhost", port=5432)))

Aynı çağrı dict ile TypeError veriyor. Denemede üç tuzak gördüm:

Tamamen değişmez JSON için json.loads(metin, object_hook=frozendict, array_hook=tuple) çalışıyor.

Diğer üç eklemeyi de denedim:

Tachyon ve JIT’e ne kadar güvenebilirsiniz?

Bu ikisini ölçmedim. Aşağıdakiler belgelerin söyledikleri ve bilinen sınırlardır.

Tachyon (profiling.sampling, PEP 799), çalışan süreci dışarıdan örnekleyen profilcidir. Belgesine göre hedef kodda değişiklik veya yeniden başlatma gerekmez, varsayılan modda süreç durdurulmaz, ek yük “neredeyse sıfır”dır.

python3.15 -m profiling.sampling attach --flamegraph -d 30 -o profil.html 12345

Sınırları şunlar:

JIT hâlâ deneysel. What’s New pyperformance sonuçlarını şöyle veriyor: x86-64 Linux’ta, tüm optimizasyonlarla derlenmiş standart yorumlayıcıya karşı %7-8 geometrik ortalama; AArch64 macOS’ta, kuyruk çağrılı yorumlayıcıya karşı %11-12 hız artışı. Tekil testlerde aralık yaklaşık %15 yavaşlamadan %100’ün üstünde hızlanmaya kadar uzanıyor. Kullandığım derlemede PYTHON_JIT=1 ile sys._jit.is_enabled() True döndü. Hız farkını ölçmedim, çünkü yüklü bir makinedeki kısa döngü bu büyüklükteki farkı ayıramaz. Kendi iş yükünüzde, boşta bir makinede karşılaştırın.

Python 3.15’e hangi sırayla yükseltmeli?

Acele etmeniz gerekmiyor. 3.14 düzenli düzeltmeleri Ekim 2027’ye kadar alıyor (PEP 745). Geçiş için şu sırayı öneririm:

  1. 3.14’te temizleyin. Uyarıları hataya çevirip testleri çalıştırın.
  2. Sessiz değişiklikleri arayın. grep komutuyla argparse, base64 ve __cached__ kullanımlarını gözden geçirin.
  3. Bağımlılıkları hedef platformda çözün. --only-binary :all: ile kurulabildiğini görün, taban imaj gerekiyorsa bu adımda güncelleyin.
  4. 3.15.2’yi CI’a ekleyin. Canlıya almadan önce 3.14 ile yan yana çalıştırın.
  5. Yerel ayarı test edin. UTF-8 olmayan bir ortamınız varsa testleri orada da çalıştırın.
  6. Karışık dönemi planlayın. Dosya paylaşan servisleri ancak açık encoding düzeltmesinden sonra ayrı ayrı yükseltin.
  7. Lazy import’u en sona bırakın. Yükseltmeyle aynı değişikliğe koymayın, başlangıç ölçümüyle ayrı bir değişiklik olarak ekleyin.

Kontrol listesi

Sonuç

Python 3.15’in en görünür özelliği lazy import, ama yükseltme riskini belirleyen o değil. Lazy import isteğe bağlı ve etkisi ölçülebilir: Kullanılmayan işi ortadan kaldırıyor, kullanılanı hızlandırmıyor. Risk, siz hiçbir şey seçmeden devreye giren değişikliklerde: UTF-8 varsayılanı, uyarı vermeden değişen davranışlar, daha yeni glibc isteyen wheel’ler ve başarılı görünen ama C uzantısını kaybeden kurulumlar.

Bunların çoğunu yükseltmeden önce, 3.14 üzerinde bulabilirsiniz. Kalanı için tek gerçek sınama, kendi testlerinizi kendi yerel ayarınızda 3.15.2 ile çalıştırmak.

Birincil kaynaklar