Data nyata tidak selalu berbentuk rapi. Sebuah harga mungkin berupa angka, tetapi kolom bebasnya kadang berupa teks; hasil pencarian user mungkin ada, mungkin tidak ada. Anotasi satu tipe seperti str tidak bisa menggambarkan dua kemungkinan tersebut. Union type menjawab kebutuhan ini dengan cara yang jujur: sebutkan semua kemungkinan tipe secara eksplisit, lalu tunjukkan ke checker bagaimana setiap kemungkinan ditangani.

Menyatakan Beberapa Kemungkinan dengan Union #

Sejak Python 3.10, union ditulis dengan operator pipa | di antara tipe-tipe anggotanya:

python
def format_harga(harga: int | str) -> str:
    if isinstance(harga, str):
        return harga.upper()
    return f"Rp{harga:,}".replace(",", ".")

print(format_harga("murah"))
print(format_harga(150000))

Output yang Dihasilkan:

text
MURAH
Rp150.000

Mari kita telaah baris-barisnya:

  1. Baris 1 (harga: int | str): parameter harga berarti bisa menerima integer maupun string — checker menandai keduanya sebagai kemungkinan sah.
  2. Baris 2 (isinstance(harga, str)): pemeriksaan isinstance mempersempit (narrowing) kemungkinan di dalam blok if menjadi hanya str, sehingga method .upper() di baris 3 diperbolehkan.
  3. Baris 4 (blok else tersirat): setelah cabang str ditangani, checker tahu yang tersisa hanya int, sehingga format :, untuk pemisah ribuan aman dipakai.

Kombinasi union dan isinstance adalah pola paling penting di chapter ini: deklarasikan semua kemungkinan di signature, lalu persempit di badan function sebelum memakai method milik salah satu anggota. Tanpa penyempitan, checker menolak — dan penolakan tersebut kita lihat sebentar lagi.

Untuk kode yang harus mendukung Python 3.9, tersedia bentuk lama Union[int, str] dari module typing; maknanya identik dengan int | str.

Optional: Kemungkinan Tidak Ada Nilai #

Kasus khusus union yang paling sering muncul adalah “nilai, atau kosong”. Python menyatakannya dengan None:

python
def cari_user(user_id: int) -> str | None:
    database = {1: "Budi", 2: "Siti"}
    return database.get(user_id)

print(cari_user(1))
print(cari_user(99))

Output yang Dihasilkan:

text
Budi
None

Method dict.get() mengembalikan None ketika key tidak ditemukan, sehingga str | None adalah kejujuran tipe dari cari_user. Bentuk lama untuk maksud yang sama adalah Optional[str] dari module typing — sejak Python 3.10, keduanya benar-benar setara dan saling menggantikan; Optional[str] hanyalah nama lain untuk str | None, bukan “opsional boleh tidak ada parameternya”.

Idiom standar untuk parameter yang bisa dikosongkan adalah default None:

python
def sapa(nama: str | None = None) -> str:
    if nama is None:
        return "Halo, tamu!"
    return f"Halo, {nama}!"

print(sapa())
print(sapa("Siti"))

Output yang Dihasilkan:

text
Halo, tamu!
Halo, Siti!

Perbandingan nama is None sekaligus menjadi penyempit tipe: setelah cabang None ditangani, checker memperlakukan nama sebagai str murni di cabang berikutnya.

Implicit Optional Ditolak mypy #

Kebiasaan lama Python mengizinkan penulisan nama: str = None dengan harapan checker membacanya str | None. PEP 484 melarang bentuk implicit Optional ini, dan mypy kini menolaknya secara bawaan (no_implicit_optional=True). Simpan sebagai opsional.py:

python
def sapa(nama: str = None) -> str:
    if nama is None:
        return "Halo, tamu!"
    return f"Halo, {nama}!"

print(sapa())
bash
python3 -m mypy opsional.py

Output yang Dihasilkan:

text
opsional.py:1: error: Incompatible default for parameter "nama" (default has type "None", parameter has type "str")  [assignment]
opsional.py:1: note: PEP 484 prohibits implicit Optional. Accordingly, mypy has changed its default to no_implicit_optional=True
opsional.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)

Perbaikannya: tuliskan union secara eksplisit menjadi nama: str | None = None, persis seperti contoh sapa() di atas.

Lupa Menangani None #

Kekuatan union terlihat saat kita lalai. Simpan sebagai nama_user.py:

python
def cari_user(user_id: int) -> str | None:
    database = {1: "Budi", 2: "Siti"}
    return database.get(user_id)

nama = cari_user(99)
print(f"Halo, {nama.upper()}!")
bash
python3 -m mypy nama_user.py

Output yang Dihasilkan:

text
nama_user.py:6: error: Item "None" of "str | None" has no attribute "upper"  [union-attr]
Found 1 error in 1 file (checked 1 source file)

mypy menunjuk baris ke-6: variabel nama bertipe str | None, dan None tidak memiliki method upper(). Pesan ini akan benar-benar terjadi di runtime — cari_user(99) mengembalikan None, lalu AttributeError menghentikan program. Anotasi union membuat kesalahan semacam ini tertangkap sebelum eksekusi.

isinstance dan Union di Runtime #

Di sisi runtime, isinstance() sejak Python 3.10 juga menerima objek union sebagai argumen keduanya:

python
>>> isinstance(3.5, int | float)
True
>>> isinstance("kopi", int | float)
False

Pemeriksaan isinstance di badan function dengan sintaks yang sama persis dengan anotasinya — tidak ada tuple isinstance(x, (int, float)) terpisah yang harus dijaga sinkron. Satu ekspresi int | float berfungsi ganda: sebagai anotasi untuk checker dan sebagai objek pemeriksaan untuk runtime.