Python 3.15 Geliyor: lazy import, frozendict ve Varsayılan UTF-8 ile Neler Değişiyor?

Python 3.15 9 Ekim'de geliyor. lazy import ile 8 kat hızlı açılış, yerleşik frozendict, kavrayışlarda açma, sentinel ve varsayılan UTF-8'i sürüm adayında çalıştırdığımız örneklerle inceliyoruz.

Python 3.15’in kararlı sürümü 9 Ekim’de geliyor ve bu kez yenilikler gündelik kodunuza doğrudan dokunuyor. Programınız lazy import ile 8 kat daha hızlı açılabiliyor. Değiştirilemeyen sözlük frozendict artık yerleşik bir tip. Türkçe karakterlerle uğraşan herkesi ilgilendiren UTF-8 de sonunda varsayılan kodlama oluyor. Bu yazıda Python 3.15’in öne çıkan yeniliklerini, sürüm adayı (release candidate) üzerinde gerçekten çalıştırdığımız örneklerle inceliyoruz.

Bir Sürüm, Beş Büyük Değişiklik

Her yeni Python sürümü bir sürü küçük iyileştirme getirir, ama hepsi günlük işinizi değiştirmez. Python 3.15’te ise birkaç yenilik, yıllardır kendi çözümümüzü yazdığımız sorunları dilin içinde çözüyor:

Yenilik Ne işe yarar? PEP
lazy import Modülü ilk kullanıldığı ana kadar yüklemez, açılışı hızlandırır PEP 810
frozendict Değiştirilemeyen ve hash’lenebilen sözlük PEP 814
Kavrayışlarda açma [*liste for liste in listeler] gibi düzleştirme PEP 798
sentinel() “Değer verilmedi” anlamına gelen benzersiz işaretler PEP 661
Varsayılan UTF-8 open() her işletim sisteminde UTF-8 kullanır PEP 686

Bunlara ek olarak daha akıllı hata mesajları, Tachyon adlı yeni bir örneklemeli profil aracı ve daha yetenekli bir JIT derleyici de geliyor. Python 3.12’deki tip sistemi yeniliklerini hatırlarsanız, o sürüm daha çok kütüphane yazarlarını ilgilendiriyordu. 3.15 ise her seviyeden Python kullanıcısına bir şey sunuyor.

Örneklerin tamamını tek bir betikte topladık. main.py her yeniliği ayrı bir fonksiyonda gösteriyor ve sonunda lazy import’un açılış süresine etkisini ölçüyor.

Kodun Anatomisi

Proje bilgisi Yazar: Ahmet Aksoy · Tarih: 03.10.2026 Ortam: Python 3.15.0rc2 (Docker python:3.15-rc-slim), Ubuntu 24.04

1. frozendict: Dokunulmaz sözlük

Ayar nesneleri, önbellek (cache) anahtarları ve sabit tablolar için değişmeyen bir sözlük çoğu zaman tam aradığımız şeydir. Şimdiye kadar types.MappingProxyType gibi dolambaçlı yollara başvuruyorduk. Artık frozendict doğrudan kullanılabiliyor:

config = frozendict(host="localhost", port=8080)

try:
    config["port"] = 9090
except TypeError as exc:
    print(f"assignment blocked: {exc}")

# | operatörü YENİ bir frozendict döndürür, orijinali değişmez
prod = config | {"host": "gurmezin.com"}
print(f"derived config    : {prod}")

# Hash'lenebilir: sözlük anahtarı veya küme elemanı olabilir
cache = {config: "dev server", prod: "prod server"}
print(f"lookup by value   : {cache[frozendict(port=8080, host='localhost')]}")

# Çıktı:
# assignment blocked: 'frozendict' object does not support item assignment
# derived config    : frozendict({'host': 'gurmezin.com', 'port': 8080})
# lookup by value   : dev server

Son satıra dikkat edin: anahtarları farklı sırayla yazdığımız halde aynı önbellek kaydını bulduk. Çünkü karşılaştırmada sıra önemli değil. Bir ayrıntıyı da bilmekte fayda var: frozendict, dict‘in alt sınıfı değil. Bu yüzden isinstance(x, dict) kontrolü False döner. İki tipi birlikte kabul etmek istiyorsanız collections.abc.Mapping kullanın.

2. Kavrayışlarda açma: İç içe listeleri tek hamlede düzleştirin

Liste kavrayışı (list comprehension) içinde artık * ve ** ile açma (unpacking) yapabiliyorsunuz. İki katlı for yazmaya gerek kalmıyor:

weekly_tags = [["python", "asyncio"], ["regex"], ["python", "pytest"]]
defaults = [{"lang": "tr", "theme": "light"}, {"theme": "dark"}]

print([*tags for tags in weekly_tags])          # liste: düzleştir
print(sorted({*tags for tags in weekly_tags}))  # küme: tekrarları at
print({**d for d in defaults})                  # sözlük: birleştir

# Çıktı:
# ['python', 'asyncio', 'regex', 'python', 'pytest']
# ['asyncio', 'pytest', 'python', 'regex']
# {'lang': 'tr', 'theme': 'dark'}

Sözlük örneğinde aynı anahtar birden fazla kez geçtiğinde sonuncusu kazanıyor. Bu, varsayılan ayarların üzerine kullanıcı ayarlarını yazmak için birebir.

3. sentinel: “None” ile “hiç verilmedi” farkı

Bir fonksiyonun varsayılan değeri None olduğunda, kullanıcının bilerek None mı gönderdiğini yoksa hiçbir şey mi göndermediğini ayırt edemezsiniz. Klasik çözüm MISSING = object() yazmaktı, ama bu nesne ekranda <object object at 0x7f...> gibi anlamsız görünür. Yerleşik sentinel() bu işi okunaklı biçimde yapıyor:

MISSING = sentinel("MISSING")

def get_setting(settings: dict, key: str, default=MISSING):
    if key in settings:
        return settings[key]
    if default is MISSING:
        raise KeyError(f"{key!r} not found and no default given")
    return default

print(repr(MISSING))                           # MISSING
print(get_setting({"debug": False}, "timeout", None))  # None
get_setting({"debug": False}, "timeout")       # KeyError

4. Varsayılan UTF-8: Türkçe karakterler artık güvende

Bu yenilik özellikle Windows kullanıcılarını sevindirecek. Daha önce encoding parametresi verilmeden açılan dosyalar işletim sisteminin yerel kodlamasıyla okunuyordu. Bir makinede düzgün görünen “ş” ve “ğ” harfleri başka bir makinede bozulabiliyordu. Metin normalizasyonu yazımızda ele aldığımız Türkçe karakter tuzaklarının önemli bir kaynağı buydu. Python 3.15’te varsayılan artık her yerde UTF-8:

path.write_text("Işık, ğüşiöç — İstanbul")  # encoding verilmedi
print(sys.flags.utf8_mode)                    # 1

Yine de iyi alışkanlık değişmiyor: Kodunuzun eski sürümlerde de doğru çalışmasını istiyorsanız encoding="utf-8" yazmaya devam edin.

5. lazy import: Programınız 8 kat daha hızlı açılsın

Büyük bir komut satırı aracı düşünün. Kullanıcı yalnızca --help yazsa bile, dosyanın başındaki tüm import satırları çalışır ve onlarca modül belleğe yüklenir. lazy import ile modül, adı ilk kullanıldığı ana kadar yüklenmiyor:

import sys

lazy import asyncio
lazy from decimal import Decimal

print("asyncio yüklü mü?", "asyncio" in sys.modules)  # False
asyncio.sleep                                          # ilk erişim
print("ilk erişim sonrası:", "asyncio" in sys.modules) # True
print(Decimal("0.1") + Decimal("0.2"))                 # 0.3

Peki kazanç ne kadar? main.py yeni bir yorumlayıcı başlatıp beş standart modülü önce normal, sonra lazy olarak içe aktarıyor ve her birini yedi kez çalıştırıp ortanca süreyi alıyor:

EAGER_SCRIPT = "import asyncio, decimal, email.mime.multipart, http.client, json"
LAZY_SCRIPT = (
    "lazy import asyncio\nlazy import decimal\nlazy import email.mime.multipart\n"
    "lazy import http.client\nlazy import json"
)

def startup_ms(code: str, runs: int = 7) -> float:
    timings = []
    for _ in range(runs):
        start = time.perf_counter()
        subprocess.run([sys.executable, "-c", code], check=True)
        timings.append((time.perf_counter() - start) * 1000)
    return sorted(timings)[runs // 2]

# Çıktı (3.15.0rc2, Docker):
# eager imports :  164.2 ms
# lazy imports  :   20.7 ms  (7.9x faster startup)

Üç ayrı çalıştırmada oran 7,8 ile 8,2 kat arasında kaldı. Birkaç kısıtı da unutmayın: lazy yalnızca modülün en üst seviyesinde kullanılabiliyor. Fonksiyon, sınıf ya da try/except bloğu içinde ve from x import * ile kullanılamıyor. Mevcut kodu değiştirmeden denemek isterseniz python -X lazy_imports=all betik.py ile tüm içe aktarmaları tembel hale getirebilirsiniz.

Bonus: Daha akıllı hata mesajları ve Tachyon

Başka dillerden gelenlerin sık yaptığı hatalar için Python artık doğru yöntemi öneriyor:

>>> [1, 2].push(3)
AttributeError: 'list' object has no attribute 'push'. Did you mean '.append'?
>>> {}.put(1, 2)
AttributeError: 'dict' object has no attribute 'put'. Use d[k] = v.

Yeni profiling.sampling modülü (Tachyon) ise programınızı yavaşlatmadan, saniyede binlerce örnek alarak zamanın nereye gittiğini gösteriyor:

python -m profiling.sampling run betik.py

Denememizde iki fonksiyonlu küçük bir betikte zamanın %78’inin üreteç ifadesi kullanan fonksiyonda geçtiğini renkli bir tabloyla tek komutta gördük.

Nasıl Çalıştırılır?

Python 3.15 henüz kararlı sürüm olmadığı için en temiz yol Docker. Sisteminize hiçbir şey kurmadan deneyebilirsiniz:

cd 41_python315_features
docker run --rm -v "$PWD":/app -w /app python:3.15-rc-slim python main.py

uv kullanıyorsanız önce uv self update ile uv’yi güncelleyin, sonra uv run --python 3.15 main.py komutunu çalıştırın. Eski uv sürümleri yalnızca 3.15’in beta sürümlerini sunuyor. Betik, Python 3.15’ten eski bir yorumlayıcıyla çalıştırılırsa bir uyarı mesajıyla sonlanıyor. Üçüncü taraf hiçbir paket gerekmiyor.

Ne Öğrendik?

  • lazy import, kod değiştirmeden açılış süresini ciddi biçimde kısaltabiliyor. Bizim ölçümümüzde yaklaşık 8 kat. Komut satırı araçları en çok kazananlar.
  • frozendict, değişmemesi gereken veriler için güvenli, hash’lenebilir ve okunaklı bir çözüm. Önbellek anahtarı olarak da kullanılabiliyor.
  • Kavrayışlarda * ve **, düzleştirme ve birleştirme işlerini tek satıra indiriyor.
  • sentinel(), object() hilesinin okunaklı ve resmi karşılığı.
  • Varsayılan UTF-8, Türkçe karakter sorunlarının büyük kısmını kaynağında kesiyor. Yine de encoding parametresini açıkça yazmak en iyi alışkanlık olmaya devam ediyor.

Kaynaklar ve Sonraki Adımlar

Sonraki adım olarak kendi projelerinizden birini 3.15 imajında çalıştırın ve en ağır import satırlarınızı lazy yapıp açılış süresini ölçün. Kararlı sürüm çıktığında farkı kendi kodunuzda görmüş olursunuz.

Ahmet Aksoy

Not: Bu yazıda incelediğimiz kodu ve benzer projelerin kaynak kodlarını https://github.com/ahmetax/practical-python-examples adresinde bulabilirsiniz.

Bir Yanıt Bırak

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir