Sintaks anotasi tipe Python berubah lebih cepat daripada bagian bahasa lainnya: apa yang ditulis dengan typing.Union[int, str] lima tahun lalu kini cukup ditulis int | str, dan Python 3.12 menambahkan statement type serta generic class tanpa TypeVar. Materi dasar union dan Optional telah dibahas pada Chapter Union dan Optional Types. Chapter ini menyusuri evolusinya secara utuh, menguji bagian yang bekerja pada runtime, dan merangkum praktik terbaik anotasi modern hari ini.

Evolusi Sintaks Union #

Dulu, union wajib diimpor dari module typing dan dibangun dengan Union serta Optional. Sejak Python 3.10 (PEP 604), operator | pada tipe menjadi bagian bahasa, dan hasilnya bukan sekadar gula sintaks — objek union hasil int | float benar-benar bisa dipakai pada runtime:

python
from typing import Optional, Union

# gaya lama (sebelum Python 3.10)
def cari_user_lama(user_id: Union[int, str]) -> Optional[str]:
    return f"user-{user_id}"


# gaya modern (Python 3.10+)
def cari_user(user_id: int | str) -> str | None:
    return f"user-{user_id}"


print(cari_user_lama(42))
print(cari_user("budi"))

# union bekerja pada runtime, bukan sekadar anotasi
nilai = 3.14
print(isinstance(nilai, int | float))
print(isinstance(nilai, int | str))

# union adalah objek sungguhan yang bisa disimpan dalam variabel
Angka = int | float
print(Angka)
print(isinstance(nilai, Angka))

Output yang Dihasilkan:

text
user-42
user-budi
True
False
int | float
True

Mari kita bedah bagian-bagian pentingnya:

  1. Baris 4 dan 9 (Union[int, str] vs int | str): kedua bentuk menghasilkan anotasi yang setara; interpreter Python mengevaluasi int | str menjadi objek types.UnionType saat def dieksekusi, tanpa perlu impor apa pun.
  2. Baris 18-19 (isinstance(nilai, int | float)): karena union adalah objek runtime, fungsi isinstance() menerima int | float sebagai argumen kedua — satu panggilan menggantikan tuple (int, float). Nilai 3.14 lolos uji pertama dan gagal pada uji kedua.
  3. Baris 22 (Angka = int | float): union yang disimpan dalam variabel tetap berfungsi sebagai alat uji; baris print(Angka) mencetak representasinya, int | float.

Konsekuensi penting: pada kode berjalan di Python 3.10+, isinstance(x, int | None) sah. Sebaliknya, klausa except tetap hanya menerima tuple atau kelas exception tunggal — objek union tidak bisa dipakai di sana. Pada Python 3.8 dan 3.9, bentuk | pada anotasi hanya aman bila evaluasinya ditunda — tekniknya dibahas pada Chapter Menjaga Kompatibilitas Versi.

Statement type dan Generic Syntax PEP 695 #

Python 3.12 (PEP 695) menambahkan dua sintaks native: statement type untuk alias tipe dan parameter generic langsung pada class/def — keduanya bersih tanpa impor TypeVar:

python
# Python 3.12 (PEP 695): statement type dan generic syntax baru
type ID = int | str
type Item[T] = tuple[T, str]


def pertama[U](items: list[U]) -> U:
    return items[0]


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

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


print(ID)
print(Item[int])
print(pertama(["a", "b", "c"]))
print(Kotak(99).ambil())
print(Kotak.__type_params__)

Output yang Dihasilkan:

text
ID
Item[int]
a
99
(T,)

Perhatikan detailnya:

  1. Baris 2 (type ID = int | str): statement type membuat objek TypeAliasType bernama ID yang lazily dievaluasi — isi int | str tidak dievaluasi sampai benar-benar dibutuhkan, sehingga alias dapat merujuk nama yang didefinisikan lebih belakangan.
  2. Baris 3 (type Item[T] = tuple[T, str]): parameter T dideklarasikan langsung dalam kurung siku, menggantikan TypeVar("T") dari gaya lama; Item[int] menghasilkan alias generik yang terisi.
  3. Baris 6 (def pertama[U](...)): function generik kini mendeklarasikan parameternya di depan tanda kurung parameter, dan list[U] pada anotasi menandai fungsi mengembalikan elemen bertipe sama dengan isinya.
  4. Baris 10 (class Kotak[T]:): class generik memakai sintaks yang sama; atribut __type_params__ mengekspos parametrnya sebagai tuple (T,) untuk inspeksi runtime.

Implicit Optional Telah Pensiun dan typing.List Mulai Ditinggalkan #

Dua kebiasaan lama kini resmi tidak dianjurkan. Pertama, PEP 484 semula mengizinkan def set_diskon(persen: int = None) ditafsirkan sebagai int | None; kebijakan itu telah dicabut dan static type checker kini menolaknya. Simpan sebagai diskon_salah.py lalu jalankan python3 -m mypy diskon_salah.py:

python
def set_diskon(persen: int = None):
    return persen

Output yang Dihasilkan:

text
diskon_salah.py:1: error: Incompatible default for parameter "persen" (default has type "None", parameter has type "int")  [assignment]
diskon_salah.py:1: note: PEP 484 prohibits implicit Optional. Accordingly, mypy has changed its default to no_implicit_optional=True
diskon_salah.py:1: note: Use https://github.com/hauntsaninja/no_implicit_optional to automatically upgrade your codebase
Found 1 error in 1 file (checked 1 source file)

Bentuk yang benar menuliskan None secara eksplisit:

python
def set_diskon(persen: int | None = None):
    return persen

Output yang Dihasilkan (python3 -m mypy diskon_benar.py):

text
Success: no issues found in 1 source file

Kedua, alias generik pada typing seperti typing.List[int] dan typing.Dict[str, int] adalah jembatan masa lalu untuk Python 3.8; sejak Python 3.9 (PEP 585), tipe bawaan list[int] dan dict[str, int] mendukung subscript langsung. Tabel berikut merangkum posisi tiap kebiasaan hari ini:

AnotasiStatusGunakan bila
int | str, str | NoneStandar saat iniPython 3.10+ (atau anotasi ditunda)
list[int], dict[str, int]Standar saat iniPython 3.9+
type Alias = ..., class Kotak[T]Standar modernPython 3.12+
typing.Union, typing.OptionalMasih sah, gaya lamaMendukung Python < 3.10
typing.List, typing.DictUsang, disarankan bermigrasiHanya untuk Python 3.8
def f(x: int = None)Ditolak type checkerJangan — tulis int | None = None