Python typing Modülü ile Tip Açıklamaları

Selamlar, bu yazımda Python'da tip açıklamaları (type annotations) konusuna ve typing modülünün bize sunduğu araçlara bakacağız. Python yıllarca 'biz dynamic dilini severiz, tipler bizim işimiz değil' diyerek yürüdü, ama büyüyen kod tabanları, IDE desteği ve mypy gibi araçlar bu tabloyu epey değiştirdi. Açıkçası bence küçük scriptlerde gerek yok; ama 1000 satırı geçen bir projede tip ipuçları olmadan yazmak artık kendine kötülük etmek gibi. Lafı uzatmadan başlayalım.

Temel tipler ve değişken etiketleri

En sık karşımıza çıkan şey fonksiyon imzalarına tip vermek. Söz dizimi gayet sade:

def selamla(isim: str) -> str:
    return f'Merhaba, {isim}!'

def topla(a: int, b: int) -> int:
    return a + b

# Değişken etiketleri de mümkün
isim: str = 'Ayse'
yas: int = 30
aktif: bool = True

Burada -> str kısmı dönüş tipini belirtiyor. Python runtime'ı bu etiketleri zorlamaz; yani topla('a', 'b') yazsanız çalışır, sadece mypy size bağırır. Tipler bir doküman ve bir araç-yardımcısıdır, bir kelepçe değil.

Koleksiyon tipleri

Liste, sözlük ve tuple için iki ayrı söz dizimi var. Python 3.9 öncesi typing.List, sonrası ise doğrudan built-in tipler:

from typing import Sequence

# Modern stil (3.9+)
def isimleri_getir() -> list[str]:
    return ['Ali', 'Veli', 'Ayse']

def yaslari_topla(sayilar: list[int]) -> int:
    return sum(sayilar)

def kullanici_yaslari() -> dict[str, int]:
    return {'Ali': 30, 'Veli': 25}

# Sabit boyutlu tuple
def koordinat() -> tuple[float, float]:
    return (41.0082, 28.9784)

# Değişken uzunluklu tuple
def skorlar() -> tuple[int, ...]:
    return (85, 90, 78)

Ben yeni kodda her zaman built-in stilini tercih ediyorum, from typing import List artık eskiyi çağrıştırıyor. Hâlâ 3.8 destekliyorsanız mecburen typing üzerinden gidersiniz.

Optional, Union ve borulu söz dizimi

Bir değer ya str ya da None olabiliyorsa Optional kullanırız. Aslında Optional[str] ve Union[str, None] aynı şey:

from typing import Optional, Union

def kullanici_bul(uid: int) -> Optional[str]:
    kullanicilar = {1: 'Ali', 2: 'Veli'}
    return kullanicilar.get(uid)

# 3.10 ve sonrasi: bos pipe ile yazabilirsiniz
def modern_bul(uid: int) -> str | None:
    return kullanici_bul(uid)

def kimlik_isle(deger: int | str) -> str:
    return str(deger)

3.10 ile gelen pipe söz dizimi okunurluğu ciddi artırıyor. Bence yeni projede artık Union yazmaya gerek yok.

Generic'ler ve TypeVar

Bir fonksiyon herhangi bir tiple çalışıyorsa ama girdiyle çıktı arasında ilişki varsa devreye TypeVar giriyor. Şöyle ki, ilk elemanı dönen bir fonksiyon yazıyorsanız liste içinde ne varsa onu döndürmeli:

from typing import TypeVar, Generic

T = TypeVar('T')

def ilk_eleman(items: list[T]) -> T | None:
    return items[0] if items else None

# Stack sinifi - her tip icin tekrar tekrar yazmiyoruz
class Yigin(Generic[T]):
    def __init__(self) -> None:
        self._items: list[T] = []

    def ekle(self, item: T) -> None:
        self._items.append(item)

    def cikar(self) -> T:
        return self._items.pop()

string_yigin: Yigin[str] = Yigin()
string_yigin.ekle('selam')

Burada T bir 'değişken tip' gibi düşünülebilir. mypy ilk_eleman([1, 2, 3]) çağrısının int | None döndürdüğünü kendiliğinden çıkarır.

TypedDict ve Protocol

JSON benzeri sözlüklerle çalışıyorsanız TypedDict hayat kurtarır. API yanıtları için bire bir:

from typing import TypedDict, Protocol

class KullaniciDict(TypedDict):
    id: int
    isim: str
    eposta: str
    aktif: bool

def kullanici_olustur(data: KullaniciDict) -> None:
    print(f'Olusturuluyor: {data["isim"]}')

# Yapisal alt tipleme - duck typing'in tipli versiyonu
class Cizilebilir(Protocol):
    def ciz(self) -> None: ...

class Daire:
    def ciz(self) -> None:
        print('Daire ciziliyor')

def render(sekil: Cizilebilir) -> None:
    sekil.ciz()

render(Daire())  # Daire'nin Cizilebilir'den miras almasi gerekmez

Protocol özellikle kütüphane yazıyorsanız çok değerli. Kullanıcının sınıfını sizin arayüzünüzden türetmesini şart koşmuyor; sadece doğru metoda sahip olması yeterli. Java'dan gelen interface mantığı yerine Pythonic bir çözüm.

Sık karşılaşılan hatalar

  • Her yerde Any kullanmak: Tip vereyim derken Any koyarsanız mypy susar ama hiçbir şey de kazanmazsınız. Mümkünse somut tip yazın.
  • Mutable default argümana koleksiyon tipi vermek: def foo(x: list[int] = []) hâlâ Python'un en eski tuzağı; tip etiketi bunu çözmez.
  • List ve list'i karıştırmak: 3.9+'da list[int] yeterli, from typing import List artık gereksiz; aynı dosyada ikisini birden görmek dağınık durur.
  • mypy'i çalıştırmamak: Etiketleri yazıp CI'da kontrol etmiyorsanız sadece dokümantasyon yapmış olursunuz, hata yakalama tarafı boşa gider.

Kapanış

typing modülü Python'a sonradan eklenmiş olsa da artık modern kodun ayrılmaz parçası. Bence yeni başlayan biri bile fonksiyon imzalarına tip yazma alışkanlığı kazanmalı; Yığın, TypedDict, Protocol gibi konular zamanla oturur. Önce imzaları etiketleyin, sonra mypy --strict ile sıkın, gerisi kendiliğinden gelir. Umarım faydalı olur, bir sonraki yazıda görüşmek üzere.