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
TypeVar('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
{'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 / ayar | Ne denetler | Tipik hata |
|---|---|---|
| disallow_untyped_defs | Her fonksiyon ipuçlu olmalı | Function is missing a type annotation |
| no_implicit_optional | Varsayılan None ise tip Optional olmalı | Incompatible default for argument |
| warn_return_any | Any döndüren fonksiyon uyarır | Returning Any from function |
| strict_equality | Asla eşit olamayacak türleri karşılaştırma | Non-overlapping equality check |
| warn_unused_ignores | Gereksiz # type: ignore | Unused "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'))
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.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 fonksiyon | T = TypeVar("T")def ilk(x: list[T]) -> T | def ilk[T](x: list[T]) -> T |
| Generic sınıf | class 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çenek | TypeVar("S", int, float) | def f[S: (int, float)](x: S) |
| Tip takma adı | Tutar: TypeAlias = int | float | type 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 |
|---|---|---|
Final | Bu ad yeniden atanmamalı (sabit) | Yok; yalnız denetleyici uyarır |
ClassVar[int] | Sınıf düzeyi alan, örnek alanı değil | dataclass bu alanı alan saymaz |
Self | Metot kendi sınıfının (alt sınıf dahil) örneğini döndürür | Yok |
Annotated[int, ...] | Tipe ek bilgi (metadata) iliştirir | get_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'tir | Yok; 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ıyor | pip install types-requests ya da mypy --install-types |
Item "None" of "X | None" has no attribute ... | Değer None olabilir, kontrol yok | if x is None: ... ile daralt ya da erken dön |
Incompatible types in assignment | Değişkenin ilk tipi başka | Değişkeni baştan geniş tiple bildir ya da yeni ad kullan |
Function is missing a type annotation | --strict tipsiz fonksiyona izin vermez | Parametre ve dönüş tiplerini ekle |
Returning Any from function declared to return "int" | json.loads gibi Any dönen değer sızıyor | Sınırda doğrula (TypedDict/pydantic) ya da cast |
--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.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.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.