Coba tulis function yang mengambil elemen pertama sebuah list. Dengan list[int] sebagai parameter, function tersebut menolak list of string; dengan list[Any], function menerima semuanya tetapi checker kehilangan jejak tipe — nilai baliknya Any, dan kesalahan setelahnya tidak akan pernah tertangkap. Yang kita butuhkan adalah cara mengatakan: “parameter dan nilai balik bertipe yang sama, apa pun tipe itu”. Inilah pekerjaan type variable — dasar dari generics.

TypeVar dan Function Generic #

Type variable didefinisikan dengan TypeVar dari module typing, lalu dipakai sebagai parameter anotasi layaknya tipe biasa:

python
from typing import TypeVar

T = TypeVar("T")

def ambil_pertama(items: list[T]) -> T:
    return items[0]

print(ambil_pertama([1, 2, 3]))
print(ambil_pertama(["kopi", "teh"]))

Output yang Dihasilkan:

text
1
kopi

Mari kita telaah cara kerjanya:

  1. Baris 3 (T = TypeVar("T")): variabel T menjadi tempat penampung tipe — bukan tipe konkret, melainkan “tipe yang akan ditentukan pemanggil”.
  2. Baris 5 (items: list[T]) dan -> T: kedua posisi memakai T yang sama, sehingga checker menerapkan aturan “sekali T ditentukan, semua kemunculannya ikut”.
  3. Pemanggilan ambil_pertama([1, 2, 3]): mypy menyimpulkan T = int untuk pemanggilan ini — nilai balik berstatus int.
  4. Pemanggilan ambil_pertama(["kopi", "teh"]): T disimpulkan str — function yang sama, kontrak tipe yang berbeda sesuai pemanggil.

Inilah perbedaan mendasar dengan Any: generic mempertahankan hubungan antar-posisi. Nilai yang masuk sebagai str keluar sebagai str, dan checker tetap mengawasinya.

Class Generic dengan Generic[T] #

Mekanisme yang sama bekerja untuk class melalui pewarisan Generic[T]:

python
from typing import Generic, TypeVar

T = TypeVar("T")

class Kotak(Generic[T]):
    def __init__(self, isi: T) -> None:
        self.isi = isi

    def ambil(self) -> T:
        return self.isi

kotak_angka = Kotak(42)
kotak_nama = Kotak("Budi")
print(kotak_angka.ambil() + 8)
print(kotak_nama.ambil().upper())

Output yang Dihasilkan:

text
50
BUDI

Saat Kotak(42) dibuat, checker mengunci T = int untuk object kotak_angka — method .ambil() milik kotak_angka pasti mengembalikan int, sehingga + 8 aman. Untuk kotak_nama, T = str, sehingga .upper() sah. Tidak ada satu pun pemeriksaan runtime di kode ini; seluruh jaminan dikerjakan checker saat membaca kode.

Membatasi TypeVar dengan bound= #

TypeVar tanpa batas menerima tipe apa pun — termasuk tipe yang tidak punya method yang kita butuhkan. Misalkan function bersuara() memanggil hewan.suara(); kita perlu memastikan T selalu punya method tersebut. Solusinya: bound=:

python
from typing import TypeVar

class Hewan:
    def suara(self) -> str:
        return "..."

class Kucing(Hewan):
    def suara(self) -> str:
        return "Meong"

H = TypeVar("H", bound=Hewan)

def bersuara(hewan: H) -> str:
    return hewan.suara()

print(bersuara(Kucing()))

Output yang Dihasilkan:

text
Meong

TypeVar H dibatasi pada Hewan dan seluruh subclass-nya, sehingga checker menjamin method .suara() selalu tersedia di dalam badan bersuara. Keuntungan memakai generic (dibanding sekadar hewan: Hewan) adalah tipe asli terjaga: jika kelak Kucing punya method .lompat(), nilai balik dan penelusuran tipe tidak “menyusut” menjadi Hewan.

Constrained TypeVar: Pilihan Tertutup #

Bentuk kedua pembatasan adalah constrained — TypeVar yang hanya menerima beberapa tipe pilihan:

python
from typing import TypeVar

Angka = TypeVar("Angka", int, float)

def dua_kali(nilai: Angka) -> Angka:
    return nilai * 2

print(dua_kali(21))
print(dua_kali(3.5))

Output yang Dihasilkan:

text
42
7.0

TypeVar Angka hanya boleh diisi int atau float — tidak lebih. Checker menolak tipe lain. Simpan sebagai angka.py:

python
from typing import TypeVar

Angka = TypeVar("Angka", int, float)

def dua_kali(nilai: Angka) -> Angka:
    return nilai * 2

print(dua_kali("kopi"))
bash
python3 -m mypy angka.py

Output yang Dihasilkan:

text
angka.py:8: error: Value of type variable "Angka" of "dua_kali" cannot be "str"  [type-var]
Found 1 error in 1 file (checked 1 source file)

Kapan Generic, Kapan Any #

SituasiPilihan tepatAlasan
Tipe masuk dan keluar harus sama (identitas terjaga)TypeVarChecker menjaga hubungan antar-posisi
Tipe dibatasi pada satu keluarga classbound=Method dasar dijamin tersedia
Hanya beberapa tipe konkret sahconstrained TypeVarDaftar putih tipe ditegakkan
Benar-benar bebas tipe dan tidak diperiksaAnyJujur menyatakan “bagian ini tak diperiksa”
Lupa masih tahap prototipetanpa anotasiSama dengan Any menurut gradual typing

Any bukan musuh — Any adalah katup yang jujur untuk kode lama dan batas library. Yang perlu dihindari adalah memakai Any di tempat yang sebenarnya bisa dinyatakan dengan generic, karena setiap Any yang bisa dihapus membuat checker melihat lebih jauh.