Ana içeriğe geç
Kesit Python
İleri · Mimari

İleri tip sistemi: Generic, Protocol, TypedDict ve mypy

Generic/TypeVar, Protocol, Literal, TypedDict, overload, NewType, Callable ve ParamSpec ile kodun sözleşmesini yaz; mypy/pyright'ın nasıl çalıştığını ve strict modu öğren.

Uygulamada aç, 23 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?

Orta seviyede tip ipuçlarını (type hints) belgeleme aracı olarak kullandın. Kurumsal projelerde ipuçları bundan fazlasıdır: CI'da çalışan mypy ya da pyright, kod çalışmadan önce "bu fonksiyona None gelebilir" ya da "bu sözlükte böyle bir anahtar yok" der. Ilgaz Bank'ta bir müşteri numarasıyla hesap numarasını karıştıran kod, test edilmediyse üretimde patlar; NewType ile bu karışıklık derleme-benzeri aşamada yakalanır. Bu ders, tip sistemini bir sözleşme dili olarak kullanmayı öğretir. Unutma: Python çalışma zamanında ipuçlarını denetlemez; bu yüzden alıştırmalarda ipuçlarını typing.get_type_hints ve inspect.signature ile okuyarak kendimiz kontrol edeceğiz.

TypeVar ve Generic

Bir fonksiyon "ne verirsen onu döndürür" diyorsa Any yazmak bilgiyi kaybettirir. TypeVar, "burada bir tür var, adı T, giriş ve çıkışta aynı" der. Sınıfları da generic yapabilirsin: Stack[int]. Python 3.12 ile def ilk[T](...) ve class Stack[T]: kısa sözdizimi geldi; eski sözdizimini de tanıman gerekir çünkü kurumsal kodun çoğu onunla yazıldı.

from typing import TypeVar, Generic, get_type_hints

T = TypeVar('T')

def ilk(dizi: list[T], varsayilan: T) -> T:
    return dizi[0] if dizi else varsayilan

class Stack(Generic[T]):
    def __init__(self) -> None:
        self._v: list[T] = []
    def push(self, x: T) -> None:
        self._v.append(x)
    def pop(self) -> T:
        return self._v.pop()
    def __len__(self) -> int:
        return len(self._v)

# Python 3.12 kısa sözdizimi
def son[U](dizi: list[U]) -> U:
    return dizi[-1]

s: Stack[str] = Stack()
s.push('TR01'); s.push('TR02')
print(s.pop(), len(s))
print(ilk([], 0), ilk([3.5], 0.0), son(['a', 'b']))
print(get_type_hints(ilk))          # çalışma zamanında ipuçlarını oku
Sınırlı TypeVarTypeVar('N', int, float) yalnızca bu türlere izin verir; TypeVar('C', bound=Comparable) ise "bu Protocol'ü sağlayan her tür" der. sorted benzeri fonksiyonlar bound ile yazılır.

Literal, TypedDict, NewType

Literal['TRY', 'USD'] bir parametrenin yalnızca belirli değerleri alabileceğini söyler; enum kadar ağır değildir. TypedDict JSON'dan gelen sözlüklerin anahtarlarını ve tiplerini tanımlar; mypy yanlış anahtarı yakalar, çalışma zamanında ise sıradan dict'tir. NewType aynı temel türden ayrı kimlikler üretir: MusteriNo ve HesapNo ikisi de int olsa bile birbirinin yerine geçemez.

from typing import Literal, TypedDict, NewType, NotRequired

ParaBirimi = Literal['TRY', 'USD', 'EUR']
MusteriNo = NewType('MusteriNo', int)
HesapNo = NewType('HesapNo', int)

class Islem(TypedDict):
    hesap: HesapNo
    tutar: float
    birim: ParaBirimi
    aciklama: NotRequired[str]        # isteğe bağlı anahtar

def bakiye_etkisi(i: Islem) -> float:
    kur = {'TRY': 1.0, 'USD': 1.0, 'EUR': 1.0}   # örnek
    return i['tutar'] * kur[i['birim']]

i: Islem = {'hesap': HesapNo(1001), 'tutar': 250.0, 'birim': 'TRY'}
print(bakiye_etkisi(i), type(i).__name__)
print(Islem.__required_keys__, Islem.__optional_keys__)
m = MusteriNo(1001)
print(m == 1001, type(m))             # çalışma zamanında sıradan int
Çalışma zamanı denetlemez{'hesap': 1, 'tutar': 'yüz', 'birim': 'XXX'} yazsan Python ses çıkarmaz; yalnızca mypy uyarır. Dış veriyi (API, dosya) gerçekten doğrulamak için pydantic ya da elle kontrol gerekir; TypedDict bir niyet beyanıdır.

overload: girdiye göre dönüş tipi

Bir fonksiyon str alınca str, list[str] alınca list[str] döndürüyorsa tek imza bunu anlatamaz. @overload ile "görünür imzaları" sıralar, en sonda gerçek gerçekleştirmeyi yazarsın. Overload gövdeleri ... olur; çalışma zamanında yalnızca son tanım kalır.

from typing import overload

@overload
def normalize(x: str) -> str: ...
@overload
def normalize(x: list[str]) -> list[str]: ...
def normalize(x):
    if isinstance(x, str):
        return x.strip().lower()
    return [s.strip().lower() for s in x]

print(normalize('  AYŞE '), normalize([' A', 'b ']))

Callable ve ParamSpec

Callable[[int, str], bool] "int ve str alıp bool döndüren" fonksiyon tipidir. Dekoratör (decorator) yazarken sorun şudur: Callable[..., T] dönüş tipini korur ama parametre bilgisini yutar; editör artık yanlış argümanı göremez. ParamSpec parametre listesini bir değişken gibi taşır ve dekoratör imzayı olduğu gibi aktarır.

from typing import Callable, ParamSpec, TypeVar
from functools import wraps
import inspect

P = ParamSpec('P')
R = TypeVar('R')

def logla(fn: Callable[P, R]) -> Callable[P, R]:
    @wraps(fn)
    def sarmal(*args: P.args, **kwargs: P.kwargs) -> R:
        print(f'-> {fn.__name__}{args}{kwargs or ""}')
        return fn(*args, **kwargs)
    return sarmal

@logla
def faiz(anapara: float, oran: float, ay: int = 12) -> float:
    return round(anapara * oran * ay / 12, 2)

print(faiz(10_000, 0.3, ay=6))
print(inspect.signature(faiz))       # wraps sayesinde imza korunur

mypy ve pyright nasıl çalışır?

Her iki araç da kodu çalıştırmaz; kaynak dosyayı ayrıştırır, her ifadenin tipini çıkarım (inference) ile bulur ve ipuçlarıyla tutarsızlıkları raporlar. İpucu olmayan fonksiyon mypy için varsayılan olarak denetlenmez; --strict bunu değiştirir. Sık karşılaşılan bayraklar:

Bayrak / ayarNe denetlerTipik hata
disallow_untyped_defsHer fonksiyon ipuçlu olmalıFunction is missing a type annotation
no_implicit_optionalVarsayılan None ise tip Optional olmalıIncompatible default for argument
warn_return_anyAny döndüren fonksiyon uyarırReturning Any from function
strict_equalityAsla eşit olamayacak türleri karşılaştırmaNon-overlapping equality check
warn_unused_ignoresGereksiz # type: ignoreUnused "type: ignore" comment

pyproject.toml içinde [tool.mypy] strict = true ile hepsi açılır. Pyright (VS Code'daki Pylance) daha katı çıkarım yapar ve daha hızlıdır; ekipler genelde birini CI'da zorunlu kılar. Dar daraltma (narrowing) kavramını öğren: if x is None: return satırından sonra denetleyici x'in artık None olmadığını bilir; isinstance, in, assert ve match de daraltır.

from typing import TypeGuard, assert_never, Literal

Durum = Literal['acik', 'kapali', 'askida']

def pozitif_floatlar(v: list[object]) -> TypeGuard[list[float]]:
    return all(isinstance(x, (int, float)) and x > 0 for x in v)

def etiket(d: Durum) -> str:
    match d:
        case 'acik': return 'Aktif'
        case 'kapali': return 'Kapalı'
        case 'askida': return 'Askıda'
        case _: assert_never(d)      # yeni değer eklenirse mypy uyarır

veri: list[object] = [1.5, 2, 3.0]
if pozitif_floatlar(veri):
    print(sum(veri))                 # artık list[float]
print(etiket('askida'))
Aşamalı sıkılaştırmaBüyük eski kod tabanında strict'i bir anda açma. Önce check_untyped_defs, sonra modül modül disallow_untyped_defs; her adımda hata sayısını CI'da ölç. Şu an geçmeyen dosyaları [[tool.mypy.overrides]] ile geçici muaf tut.
Derinleş

Python 3.12 tip sözdizimi

Derste TypeVar ve Generic ile generic yazdın. Python 3.12 (PEP 695) aynı şeyi çok daha kısa yazmanı sağlar: tip parametresi köşeli parantezle fonksiyonun ya da sınıfın adının yanına yazılır, tip takma adı (type alias) için type deyimi gelir. Eski yazım çalışmaya devam eder; yeni projede yeni yazımı, 3.11 ve öncesini destekleyen kütüphanede eskisini kullan.

AmaçEski yazım (3.8+)3.12 yazımı
Generic fonksiyonT = TypeVar("T")
def ilk(x: list[T]) -> T
def ilk[T](x: list[T]) -> T
Generic sınıfclass Kutu(Generic[T])class Kutu[T]:
Üst sınır (bound)TypeVar("N", bound=Sayi)def f[N: Sayi](x: N)
Kısıtlı seçenekTypeVar("S", int, float)def f[S: (int, float)](x: S)
Tip takma adıTutar: TypeAlias = int | floattype Tutar = int | float
from collections.abc import Iterable, Callable

type Tutar = int | float
type Kural[T] = Callable[[T], bool]

def ilk_uygun[T](ogeler: Iterable[T], kural: Kural[T]) -> T | None:
    for x in ogeler:
        if kural(x):
            return x
    return None

class Kutu[T]:
    def __init__(self, icerik: T) -> None:
        self.icerik = icerik

print(ilk_uygun([120, 900, 4500], lambda t: t > 500))
print(Tutar.__value__, Kutu.__type_params__, ilk_uygun.__type_params__)

Varyans: list[int] neden list[float] değildir?

int, float beklenen yere verilebilir. Ama list[int], list[float] beklenen yere verilemez: fonksiyon listeye 3.5 eklerse senin "tamsayı listen" bozulur. Değiştirilebilir kaplar bu yüzden değişmez (invariant)dir. Yalnız okunan kaplar (Sequence, Mapping, Iterable) eşdeğişkendir (covariant): Sequence[int], Sequence[float] yerine geçer. Kural: fonksiyon parametresinde ihtiyacın olan en dar yeteneği iste; yalnız okuyorsan list değil Sequence/Iterable yaz, böylece çağıran demet, liste ya da üreteç (generator) verebilir.

def ortalama(tutarlar: list[float]) -> float: ...
def ortalama2(tutarlar: Sequence[float]) -> float: ...

adetler: list[int] = [3, 5, 8]
ortalama(adetler)    # mypy: Argument 1 has incompatible type "list[int]"; expected "list[float]"
ortalama2(adetler)   # geçerli: Sequence eşdeğişkendir

Küçük ama değerli araçlar

AraçNe söyler?Çalışma anında etkisi
FinalBu ad yeniden atanmamalı (sabit)Yok; yalnız denetleyici uyarır
ClassVar[int]Sınıf düzeyi alan, örnek alanı değildataclass bu alanı alan saymaz
SelfMetot kendi sınıfının (alt sınıf dahil) örneğini döndürürYok
Annotated[int, ...]Tipe ek bilgi (metadata) iliştirirget_type_hints(..., include_extras=True) ile okunur; pydantic ve FastAPI kullanır
TypeGuard[X] / TypeIs[X]bool dönen fonksiyon, True ise argüman X'tirYok; TypeIs 3.13+ (öncesinde typing_extensions)
assert_never(x)Bu satıra hiçbir tip ulaşamamalıUlaşılırsa AssertionError
NotRequired[...]TypedDict'te isteğe bağlı anahtar__optional_keys__'ta görünür
from typing import Annotated, Literal, assert_never, get_type_hints
from dataclasses import dataclass

@dataclass(frozen=True)
class Aralik:
    alt: float
    ust: float

def faiz(oran: Annotated[float, Aralik(0, 0.6)], ay: int) -> float:
    return round(oran / 12 * ay, 4)

ipucu = get_type_hints(faiz, include_extras=True)["oran"]
print(ipucu.__origin__, ipucu.__metadata__)

type Kanal = Literal["sube", "mobil", "atm"]

def komisyon(kanal: Kanal) -> float:
    match kanal:
        case "sube":
            return 5.0
        case "mobil":
            return 0.0
        case "atm":
            return 2.5
        case _:
            assert_never(kanal)   # yeni kanal eklenip burası unutulursa mypy hata verir

print(komisyon("atm"))
try:
    komisyon("faks")              # tip denetleyici bunu zaten reddeder
except AssertionError as h:
    print("AssertionError:", str(h)[:30])

Self ve ClassVar: akıcı sınıflarda doğru dönüş tipi

Bir metot return self yapıyorsa dönüş tipini sınıf adıyla yazmak alt sınıflarda yanlış olur: OzelSorgu().filtre(...) tip denetleyiciye göre Sorgu döner ve alt sınıfın metotları "yok" görünür. Self "hangi sınıftan çağrıldıysa o" demektir. ClassVar ise alanın sınıfa ait olduğunu söyler; dataclass onu kurucu parametresi yapmaz.

from dataclasses import dataclass, field, fields
from typing import ClassVar, Self

@dataclass
class Sorgu:
    tablo: str
    kosullar: list[str] = field(default_factory=list)
    olusturulan: ClassVar[int] = 0          # alan değil, sınıf sayacı

    def __post_init__(self) -> None:
        Sorgu.olusturulan += 1

    def filtre(self, kosul: str) -> Self:
        self.kosullar.append(kosul)
        return self

class SiparisSorgusu(Sorgu):
    def buyukler(self) -> Self:
        return self.filtre("tutar > 1000")

s = SiparisSorgusu("siparis").filtre("sehir = 'İzmir'").buyukler()
print(type(s).__name__, s.kosullar, Sorgu.olusturulan)
print("kurucu alanları:", [f.name for f in fields(Sorgu)])   # ClassVar yok

mypy'ı projeye kurmak

Tip ipuçları ancak bir denetleyici çalıştırıldığında işe yarar. Aşağıdaki adımları kendi bilgisayarında, projenin sanal ortamında (virtual environment) uygula (tarayıcıda çalışmaz).

pip install mypy                 # ya da: pip install pyright
mypy src/                        # klasörü denetle
mypy --strict src/               # en sıkı mod: tipsiz fonksiyon, Any sızıntısı yasak

# pyproject.toml
[tool.mypy]
python_version = "3.12"
strict = true
warn_unused_ignores = true
[[tool.mypy.overrides]]
module = ["eski_modul.*"]
ignore_errors = true             # eski kodu kademeli tiplemek için

# kodun içinde
reveal_type(sonuc)               # mypy çıktısında çıkarılan tipi gösterir
x = eski_fn()  # type: ignore[no-untyped-call]   # her zaman hata koduyla sustur
mypy mesajıAnlamıÇözüm
Library stubs not installed for "requests"Paketin tip bilgisi ayrı dağıtılıyorpip install types-requests ya da mypy --install-types
Item "None" of "X | None" has no attribute ...Değer None olabilir, kontrol yokif x is None: ... ile daralt ya da erken dön
Incompatible types in assignmentDeğişkenin ilk tipi başkaDeğişkeni baştan geniş tiple bildir ya da yeni ad kullan
Function is missing a type annotation--strict tipsiz fonksiyona izin vermezParametre ve dönüş tiplerini ekle
Returning Any from function declared to return "int"json.loads gibi Any dönen değer sızıyorSınırda doğrula (TypedDict/pydantic) ya da cast
Kademeli geçişBüyük bir kod tabanına bir günde --strict uygulanmaz. Önce varsayılan modla CI'a ekle, yeni dosyaları strict tut (overrides), her sprintte bir modülü tiple. warn_unused_ignores artık gereksiz olan susturmaları gösterir.
Gerçek işteTip ipucunun en büyük faydası, kodu değiştirirken gelir: bir fonksiyonun dönüş tipini float'tan Decimal'e çevirdiğinde mypy etkilenen her çağrıyı saniyeler içinde listeler. Pull request'te CI'ın mypy adımı kırmızıysa, bu çoğu zaman üretimde patlayacak bir None hatasının erken uyarısıdır.
Uzmanlaş

Tipleri sözleşme olarak kullanmak

Olgun ekiplerde tip ipuçları belgelemeden çok tasarım aracıdır. Bir fonksiyon dict[str, Any] alıyorsa bu genelde "şeklini bilmiyorum" itirafıdır; TypedDict ya da dataclass'a çevirmek sorunun yarısını çözer. Optional parametre çokluğu fonksiyonun iki iş yaptığına işarettir. NewType ile kimlikleri ayırmak, Ilgaz Bank örneğindeki gibi "müşteri no yerine hesap no gönderme" hatasını tamamen ortadan kaldırır.

Variance (değişinti) konusunu bil: list[int], list[float]'ın alt türü değildir (liste değişebilir, invariant); ama Sequence[int], Sequence[float] yerine geçer (salt okunur, covariant). Parametre tiplerinde olabildiğince genel soyut tip (Sequence, Mapping, Iterable), dönüşte somut tip (list, dict) kullanmak bu sorunların çoğunu önler.

Çalışma zamanı doğrulaması gerekiyorsa pydantic, TypedDict'in doğal devamıdır: aynı alan tanımı hem denetleyiciye hem çalışma zamanına hizmet eder. Ancak iç fonksiyonlarda doğrulama tekrarı maliyetlidir; doğrulamayı sınırda (API girişi, dosya okuma) yap, içeride tiplere güven.

Alıştırmalara geç (23)