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
encodingparametresini açıkça yazmak en iyi alışkanlık olmaya devam ediyor.
Kaynaklar ve Sonraki Adımlar
- What’s New in Python 3.15: resmi yenilikler listesi
- Python 3.15.0rc3 duyurusu
- PEP 810 (lazy import), PEP 814 (frozendict), PEP 798 (kavrayışlarda açma), PEP 661 (sentinel), PEP 686 (UTF-8)
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.



