Python'da tip notları (type hints), bir değişkenin, parametrenin veya dönüş değerinin hangi tipte olmasının beklendiğini x: int ve -> str gibi işaretlerle belirtmenizi sağlar. En önemli kural şudur: Python yorumlayıcısı bu notları çalışma zamanında zorlamaz. typing belgesinin en başındaki not da bunu söyler: notlar tip denetleyicileri, IDE'ler ve linter'lar gibi üçüncü taraf araçlar tarafından kullanılır. Yani tip notları programın davranışını değiştirmez, hataları kod çalışmadan önce yakalamanıza ve kodu belgelemenize yardım eder.

Bu yazıda temel sözdizimini, yerleşik koleksiyonlarla list[int] yazımını, X | None birleşimlerini, 3.12 ile gelen type deyimi ve generic sözdizimini, TypedDict, Protocol, NewType, Literal gibi araçları ve Python 3.14'te değişen ertelenmiş annotation değerlendirmesini ele alıyoruz. Örnekler Python 3.15.2 ile çalıştırıldı. Serinin bütünü için Python öğrenme rehberi sayfasına bakabilirsiniz.

Temel sözdizimi ve "zorlanmaz" kuralı

Temel sözdizimi ve "zorlanmaz" kuralı

Parametreden sonra iki nokta üst üste ve tip, parametre listesinden sonra -> ve dönüş tipi yazılır. Değişkenler için de stok: dict[str, int] = {} biçimi kullanılır. Aşağıdaki örnekte kare fonksiyonu int beklediğini söylüyor, ama 2.5 verdiğimizde Python hiçbir itirazda bulunmadan 6.25 döndürüyor. Notlar fonksiyonun __annotations__ özniteliğinde saklanır; tip denetleyicisi ise bu çağrıyı, kod çalışmadan önce, hata olarak işaretler.

python
def kare(x: int) -> int:
    return x * x

print(kare(3))
print(kare(2.5))
print(kare.__annotations__)

# çıktı: 9
# çıktı: 6.25
# çıktı: {'x': <class 'int'>, 'return': <class 'int'>}

Bu davranış hem güç hem sorumluluktur. Dış dünyadan (kullanıcı girdisi, JSON, veritabanı) gelen verinin tipini çalışma zamanında gerçekten doğrulamanız gerekiyorsa, tip notuna güvenmek yerine isinstance() kontrolü yapmanız ya da bir doğrulama katmanı kullanmanız gerekir. Değişkenlerin ve tiplerin genel mantığı için Python değişkenler ve veri tipleri yazısına bakabilirsiniz.

Koleksiyonlar, birleşimler ve None

Python 3.9'dan beri (PEP 585) yerleşik list, dict, tuple, set gibi sınıflar köşeli parantezle parametrelenebilir: list[int], dict[str, float]. Eski typing.List, typing.Dict gibi takma adlar 3.9'dan beri kullanımdan kaldırılmış (deprecated) durumdadır; belgeye göre şimdilik kaldırılmaları planlanmıyor ve yorumlayıcı uyarı vermiyor, ama yeni kodda yerleşik adları kullanmanız önerilir.

Tuple'lar listelerden farklı not edilir: tuple[int, int] tam olarak iki tamsayılık bir tuple anlamına gelir, tuple[str, ...] ise herhangi uzunlukta str tuple'ıdır. Liste ise tek tip argüman alır; list[int, str] yazmak tip denetleyicisinde hatadır. Birden fazla olası tip için Python 3.10'dan beri int | str yazabilirsiniz; Optional[X] da X | None ile eşdeğerdir.

python
def ortalama(notlar: list[float]) -> float | None:
    if not notlar:
        return None
    return sum(notlar) / len(notlar)

stok: dict[str, int] = {'elma': 3}
nokta: tuple[int, int] = (1, 2)
etiketler: tuple[str, ...] = ('a', 'b', 'c')
print(ortalama([70, 80, 90]))
print(ortalama([]))
print(list[int], int | None)
print(isinstance(5, int | str))

# çıktı: 80.0
# çıktı: None
# çıktı: list[int] int | None
# çıktı: True

Belge Optional ile ilgili önemli bir ayrımı da vurgular: "isteğe bağlı argüman" (varsayılan değeri olan parametre) ile "None olabilen değer" aynı şey değildir. def f(arg: int = 0) parametresi isteğe bağlıdır ama None kabul etmez, bu yüzden Optional gerekmez. None açıkça izinli bir değerse int | None yazmak doğrudur. Fonksiyon gövdesinde böyle bir değeri kullanmadan önce if deger is None kontrolü yapmanız, tip denetleyicisinin tipi daraltmasını (narrowing) sağlar.

Python 3.14: Union tek bir tipte birleşti

Python 3.14 What's New belgesine göre typing.Union ile types.UnionType artık birbirinin takma adıdır. Hem Union[int, str] hem int | str aynı çalışma zamanı tipini üretir ve repr() çıktısı her ikisi için int | str olur. Eski yazımla üretilen birleşimler artık önbelleğe alınmadığı için iki birleşimi is ile değil, == ile karşılaştırmanız gerekir.

python
from typing import Union

try:
    isinstance([1, 2], list[int])
except TypeError as e:
    print(type(e).__name__ + ':', e)
print(repr(Union[int, str]))
print(Union[int, str] == (int | str))

# çıktı: TypeError: isinstance() argument 2 cannot be a parameterized generic
# çıktı: int | str
# çıktı: True

Örneğin ilk satırı başka bir kurala dikkat çekiyor: isinstance() ikinci argüman olarak parametrelenmiş generic kabul etmez. Bir değerin liste olduğunu kontrol edebilirsiniz, ama "tamsayı listesi" olduğunu tek bir isinstance çağrısıyla kontrol edemezsiniz; öğeleri ayrıca dolaşmanız gerekir.

type deyimi ve generic sözdizimi (3.12+)

Karmaşık tipleri tekrar tekrar yazmamak için tip takma adı tanımlarsınız. Güncel typing belgesi bunu type deyimiyle anlatır: type Koordinat = tuple[float, float] satırı bir TypeAliasType örneği oluşturur ve tip denetleyicileri bu adı hedef tiple eşdeğer kabul eder. Takma adın değeri tembel değerlendirilir; gerçek değere __value__ ile ulaşılır.

Python 3.12 ile generic fonksiyon ve sınıflar için köşeli parantezli tip parametresi sözdizimi geldi: def ilk[T](ogeler: list[T]) -> T yazımı, "hangi tipte liste verirseniz o tipte öğe dönecek" demektir. Sınıflar için de class Kutu[T]: yazılır ve böyle bir sınıf örtük olarak Generic sınıfından türer. 3.11 ve öncesiyle uyumluluk gerekiyorsa TypeVar ve Generic[T] ile eski yazım hâlâ geçerlidir.

python
type Koordinat = tuple[float, float]

def ilk[T](ogeler: list[T]) -> T:
    return ogeler[0]

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

    def al(self) -> T:
        return self.icerik

print(ilk([3, 4]))
print(Kutu('kitap').al())
print(type(Koordinat).__name__, Koordinat.__value__)
print(ilk.__type_params__)
print(Kutu[int])

# çıktı: 3
# çıktı: kitap
# çıktı: TypeAliasType tuple[float, float]
# çıktı: (T,)
# çıktı: __main__.Kutu[int]

Tip parametresine sınır da verebilirsiniz: [T: Sequence[bytes]] T'nin bir bayt dizisi alt tipi olmasını, [S: (int, str)] ise yalnızca bu iki tipten biri olmasını ister. Generic yapıların yanında soyut koleksiyon tiplerini (Sequence, Mapping, Iterable) tercih etmek de iyi bir alışkanlıktır: belge, argümanları not ederken somut dict yerine Mapping gibi soyut tipleri önerir; böylece fonksiyonunuz daha geniş bir girdi yelpazesini kabul eder.

Any ve object: kaçış kapısı ile güvenli genellik

Any ve object: kaçış kapısı ile güvenli genellik

Any özel bir tiptir: tip denetleyicisi her tipi Any'ye, Any'yi de her tipe atanabilir kabul eder ve Any tipli bir değer üzerinde yapılan hiçbir işlemi denetlemez. Not yazılmamış parametreler ve dönüş değerleri de örtük olarak Any sayılır. Bu, dinamik ve statik tipli kodu karıştırırken işe yarayan bir kaçış kapısıdır.

object ise ilk bakışta benzer görünse de tam tersini yapar: her tip object'in alt tipidir, ama object tipli bir değer üzerinde denetleyici neredeyse hiçbir işleme izin vermez. Belgenin özeti şöyledir: değer "herhangi bir şey olabilir" ve bunu güvenli biçimde ifade etmek istiyorsanız object, değer gerçekten dinamik tipliyse Any kullanın.

TypedDict: belirli anahtarları olan sözlükler

JSON verisi gibi sabit anahtarlı sözlükleri not etmek için TypedDict kullanılır. Çalışma zamanında bu nesneler sıradan dict'tir; anahtar ve değer tipleri yalnızca tip denetleyicisi tarafından kontrol edilir. Varsayılan olarak bütün anahtarlar zorunludur; tek tek anahtarları NotRequired ile isteğe bağlı yapabilir ya da sınıfa total=False vererek hepsini isteğe bağlı hale getirebilirsiniz.

python
from typing import TypedDict, NotRequired

class Kullanici(TypedDict):
    ad: str
    yas: int
    eposta: NotRequired[str]

k: Kullanici = {'ad': 'Ali', 'yas': 30}
print(type(k).__name__, k)
print(sorted(Kullanici.__required_keys__), sorted(Kullanici.__optional_keys__))

# çıktı: dict {'ad': 'Ali', 'yas': 30}
# çıktı: ['ad', 'yas'] ['eposta']

Python 3.15 ile gelen PEP 728, TypedDict sınıflarına closed ve extra_items argümanlarını ekledi. closed=True tanımlı anahtarlar dışında anahtar kabul etmeyen kapalı bir sözlük tipi tanımlar; extra_items=str ise ek anahtarlara izin verir ama değerlerinin belirtilen tipte olmasını ister. Bu kurallar da yalnızca tip denetleyicisi içindir; çalışma zamanında sözlük yine sözlüktür. Sözlüklerin kendisi için Python sözlük (dict) metotları yazısına bakabilirsiniz.

python
from typing import TypedDict

class Nokta(TypedDict, closed=True):
    x: int
    y: int

class Etiketli(TypedDict, extra_items=str):
    ad: str

p: Nokta = {'x': 1, 'y': 2}
e: Etiketli = {'ad': 'sunucu', 'bolge': 'eu'}
print(p, e)
print(Nokta.__closed__, Etiketli.__extra_items__)

# çıktı: {'x': 1, 'y': 2} {'ad': 'sunucu', 'bolge': 'eu'}
# çıktı: True <class 'str'>

Protocol: yapısal alt tipleme (statik duck typing)

Python'un ilk tip sistemi (PEP 484) "nominal" alt tiplemeye dayanıyordu: bir sınıfın beklenen tipe uyması için ondan açıkça türemesi gerekiyordu. PEP 544 ile gelen Protocol, yapısal alt tiplemeyi mümkün kılar: bir sınıf, protokoldeki yöntemlere sahipse ondan türemeden de o protokole uyar. Bu, Python'un "ördek gibi yürüyorsa ördektir" yaklaşımının statik karşılığıdır.

python
from typing import Protocol, runtime_checkable

@runtime_checkable
class Kapatilabilir(Protocol):
    def close(self) -> None: ...

class Baglanti:
    def close(self) -> None:
        print('bağlantı kapatıldı')

def kapat(nesne: Kapatilabilir) -> None:
    nesne.close()

kapat(Baglanti())
print(isinstance(Baglanti(), Kapatilabilir))
print(isinstance(42, Kapatilabilir))

# çıktı: bağlantı kapatıldı
# çıktı: True
# çıktı: False

@runtime_checkable ile işaretlenen protokoller isinstance() içinde de kullanılabilir; ancak belge bu kontrolün yalnızca gerekli özniteliklerin var olup olmadığına baktığını, imzalarını ve tiplerini denetlemediğini açıkça belirtir. Ayrıca bu özellik kalıtımla geçmez: çalışma zamanında denetlenebilir bir protokolden türeyen alt protokol, kendisi de işaretlenmedikçe öyle sayılmaz. Python 3.15'te, işaretlenmemiş böyle bir alt protokolle isinstance() ya da issubclass() çağırmak kullanımdan kaldırıldı ve belgeye göre Python 3.20'de TypeError verecek. Sınıflar ve kalıtım için Python kalıtım, super ve MRO yazısına bakabilirsiniz.

NewType, Literal, Final ve Callable

NewType, tip denetleyicisinin ayrı bir tip gibi davrandığı ama çalışma zamanında orijinal tipe dönüşen bir yardımcıdır: KullaniciId(42) ifadesinin sonucu düz bir int'tir. Böylece kullanıcı kimliğiyle sipariş kimliğini karıştırmak gibi mantık hatalarını denetleyici yakalar. Literal yalnızca belirli değerlere izin verir, Final ise bir adın yeniden atanmaması gerektiğini bildirir. Bu kısıtların hiçbiri çalışma zamanında zorlanmaz.

python
from typing import NewType, Literal, Final

KullaniciId = NewType('KullaniciId', int)
u = KullaniciId(42)
print(u, type(u).__name__)

Yon = Literal['kuzey', 'guney']

def git(yon: Yon) -> str:
    return f'{yon} yönüne gidiliyor'

print(git('kuzey'))
SINIR: Final = 100
print(SINIR)

# çıktı: 42 int
# çıktı: kuzey yönüne gidiliyor
# çıktı: 100

Fonksiyon alan ya da döndüren fonksiyonlar için collections.abc.Callable kullanılır: Callable[[int], int] "bir int alıp int döndüren çağrılabilir" demektir. Generator fonksiyonlarının dönüş tipi genellikle Iterator[int] ya da daha ayrıntılı Generator[...] ile not edilir; generator'lar Python generator ve yield yazısında anlatılıyor. Decorator'larda imzayı korumak için ParamSpec kullanılır.

python
from collections.abc import Callable, Iterator

def uygula(fonk: Callable[[int], int], deger: int) -> int:
    return fonk(deger)

def sayac(n: int) -> Iterator[int]:
    yield from range(n)

print(uygula(lambda x: x + 1, 41))
print(list(sayac(3)))

# çıktı: 42
# çıktı: [0, 1, 2]

Python 3.14: annotation'lar artık ertelenerek değerlendiriliyor

Python 3.14 What's New belgesine göre (PEP 649 ve PEP 749), fonksiyon, sınıf ve modüllerdeki annotation'lar artık tanım anında hemen değerlendirilmiyor. Bunun yerine özel "annotate" fonksiyonlarında saklanıyor ve yalnızca gerektiğinde değerlendiriliyor (from __future__ import annotations kullanılan dosyalar hariç). Bunun en görünür sonucu şudur: henüz tanımlanmamış bir sınıfa atıf yapan notları (forward reference) artık tırnak içine almanız gerekmiyor.

Yeni annotationlib modülü notları üç biçimde okumanızı sağlar: VALUE biçimi notları gerçek değerlere çevirir (eski davranışa benzer) ve tanımsız bir ad varsa NameError verir; FORWARDREF tanımsız adları özel işaretçilerle değiştirir; STRING ise notları metin olarak döndürür. Aşağıdaki örnekte Agac.ekle metodu, kendisinden sonra tanımlanan Dugum sınıfına tırnaksız atıf yapıyor ve sorun çıkmıyor.

python
from annotationlib import get_annotations, Format

class Agac:
    def ekle(self, cocuk: Dugum) -> None: ...

class Dugum:
    pass

print(get_annotations(Agac.ekle))

def f(x: Tanimsiz) -> int: ...

print(get_annotations(f, format=Format.STRING))
try:
    get_annotations(f)
except NameError as e:
    print(type(e).__name__ + ':', e)

# çıktı: {'cocuk': <class '__main__.Dugum'>, 'return': None}
# çıktı: {'x': 'Tanimsiz', 'return': 'int'}
# çıktı: NameError: name 'Tanimsiz' is not defined

Annotations Best Practices belgesi, 3.10 ile 3.13 arasında notlara erişmenin en iyi yolunun inspect.get_annotations() olduğunu, 3.14 ve sonrasında ise bunun yerini annotationlib.get_annotations() fonksiyonunun aldığını söyler. Bilinmeyen bir nesnenin notlarına doğrudan __annotations__ ile erişmek gerekiyorsa üç argümanlı getattr(o, '__annotations__', None) önerilir. typing.get_type_hints() ise notları çözümlenmiş tiplerle döndüren klasik yardımcıdır.

python
from typing import Any, get_type_hints

def isle(veri: Any, sayi: int = 0) -> str:
    return str(veri)

print(get_type_hints(isle))

# çıktı: {'veri': typing.Any, 'sayi': <class 'int'>, 'return': <class 'str'>}

Python 3.15'te typing

3.15 What's New belgesi typing için birkaç yenilik listeler. PEP 747 ile gelen TypeForm, değerin kendisi bir tip ifadesi olduğunda (örneğin bir fonksiyona int veya list[int] geçirildiğinde) bunu kesin biçimde not etmeyi sağlar; çalışma zamanında TypeForm(x) yalnızca x döndürür. PEP 800 ile gelen @typing.disjoint_base, yerleşik ya da derlenmiş tiplerin çoklu kalıtım kısıtlarını tip denetleyicilerine aktarmak için kullanılan ileri bir decorator'dır. TypeVarTuple artık bound ve varyans argümanlarını kabul ediyor.

Aynı sürümde kaldırılanlar da var: NamedTuple sınıflarını anahtar kelime argümanlarıyla oluşturan belgelenmemiş yazım (NamedTuple("Point", x=int, y=int)) artık desteklenmiyor; alansız TypedDict("TD") yazımı da kaldırıldı, yerine class TD(TypedDict): pass ya da TypedDict("TD", {}) kullanılmalı. Sürümün tamamı için Python 3.15 yenilikleri yazısına bakabilirsiniz.

Tip denetleyicisi kullanmak

Tip notlarından gerçek fayda görmek için kodunuzu bir statik tip denetleyicisiyle çalıştırmanız gerekir. Standart kütüphanede bir tip denetleyicisi yoktur; typing belgesi topluluk tarafından geliştirilen araçlara (örneğin mypy belgelerine) ve Python tip sistemi spesifikasyonuna yönlendirir. Bu araçlar üçüncü taraf paketlerdir ve sanal ortama venv ve pip ile kurulur. Çoğu modern editör de aynı bilgiyi otomatik tamamlama ve satır içi uyarılar için kullanır. Yeni typing özelliklerini eski Python sürümlerinde kullanmak isterseniz belge, bu özelliklerin geri taşındığı typing_extensions paketini anar.

Sık yapılan hatalar

Sık yapılan hatalar: Notların çalışma zamanında zorlandığını sanmak, Optional'ı varsayılan değerle karıştırmak, list[int] ile isinstance, Union'ları is ile karşılaştırmak
  • Notların çalışma zamanında zorlandığını sanmak: def f(x: int) fonksiyonuna str geçirirseniz Python hata vermez. Doğrulama gerekiyorsa açıkça yapın.
  • Optional'ı varsayılan değerle karıştırmak: Varsayılan değeri olan parametre, None kabul etmiyorsa Optional gerektirmez.
  • list[int] ile isinstance: Parametrelenmiş generic'ler isinstance() içinde kullanılamaz; TypeError verir.
  • Union'ları is ile karşılaştırmak: 3.14'ten beri eski yazımlı birleşimler önbelleğe alınmıyor; == kullanın.
  • runtime_checkable'ın imzayı denetlediğini sanmak: Yalnızca özniteliğin varlığına bakılır.
  • Her yere Any yazmak: Any denetimi kapatır. Gerçekten bilinmeyen değerler için object daha güvenlidir.
  • Çalışma zamanında __annotations__ okuyup her şeyin değer olduğunu varsaymak: 3.14'ten beri notlar tembel değerlendiriliyor; tanımsız adlar varsa annotationlib biçimlerini kullanın.

Sık Sorulan Sorular

Tip notları programı yavaşlatır mı?

Notlar çalışma zamanında zorlanmadığı için çağrı başına bir denetim maliyeti yoktur. Python 3.14 What's New belgesi, ertelenmiş değerlendirmenin notları tanımlamanın çalışma zamanı maliyetini en aza indirdiğini belirtir.

Küçük betiklerde tip notu yazmalı mıyım?

Zorunlu değildir; not yazılmamış kod örtük olarak Any kabul edilir ve sorunsuz çalışır. Notlar özellikle fonksiyonların başkaları tarafından kullanıldığı, kodun büyüdüğü ve editör desteğinin önem kazandığı projelerde fayda sağlar. Kademeli olarak, önce dışa açık fonksiyonların imzalarından başlayarak ekleyebilirsiniz.

dataclass ile TypedDict arasında nasıl seçim yaparım?

TypedDict çalışma zamanında sıradan bir sözlüktür; JSON gibi zaten sözlük biçiminde gelen veriyi not etmek için uygundur. Davranışı (metotları) olan, öznitelik erişimiyle kullanılacak nesneler için Python dataclasses daha uygundur.

from __future__ import annotations hâlâ gerekli mi?

3.14 ve sonrasında forward reference için genellikle gerekmez, çünkü notlar zaten ertelenerek değerlendirilir. 3.7'de PEP 563 ile gelen bu ifade bütün notları metne çevirir ve 3.14'te davranışı değişmedi. Ancak What's New belgesine göre artık kullanımdan kaldırılmış durumda ve gelecekteki bir sürümde kaldırılması bekleniyor; bu, ertelenmiş değerlendirmeyi desteklemeyen son sürüm olan 3.13'ün 2029'daki destek sonundan önce olmayacak. Yalnızca 3.14 ve sonrasını destekliyorsanız bu satırı büyük olasılıkla kaldırabilirsiniz; notları okuyan üçüncü taraf kütüphanelerin de buna uyumlu olması gerekir.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Python öğrenme rehberi · dataclasses · Class ve nesne · Decorator yazma · Fonksiyon tanımlama · Python 3.15 yenilikleri