%%{init: {"flowchart": {"diagramPadding": 40}}}%%
flowchart LR
A["Kode dengan<br/>type hint"] --> B["Pemeriksa tipe<br/>(mypy / pyright)"]
B --> C{"Ada yang<br/>tidak cocok?"}
C -- "ya" --> D["Daftar kesalahan,<br/>program belum dijalankan"]
C -- "tidak" --> E["Lanjut jalankan<br/>program"]
Bab 33 — Type Hint: Memberi Label pada Data, Memeriksa Sebelum Berjalan
Seri I4 — Bahasa Python Tingkat Lanjut
Semua skrip bab ini ada di folder kode/bab33/. Di versi web, Anda juga bisa mengunduh semuanya sekaligus (zip).
Bayangkan gudang pakan dengan karung tanpa label. Anda harus membuka tiap karung untuk tahu isinya, dan kadang salah mengambil. Sekarang bayangkan tiap karung berlabel “pelet ikan, 25 kg”. Anda langsung tahu, dan petugas yang memeriksa gudang bisa menandai karung yang salah tempat sebelum pakan dipakai.
Type hint (petunjuk tipe) adalah label itu pada kode: catatan tentang jenis data yang diharapkan sebuah variabel atau fungsi. Python sendiri tidak memaksakannya saat program berjalan, tetapi manusia, editor, dan alat pemeriksa bisa memanfaatkannya untuk menemukan kesalahan sebelum program dijalankan.
Anotasi dasar
01_anotasi_dasar.py
# Bab 33 - Anotasi tipe: catatan untuk manusia dan alat, bukan paksaan
def biomassa_kg(jumlah: int, bobot_g: float) -> float:
return jumlah * bobot_g / 1000
umur_hari: int = 120
nama_kolam: str = "A"
print(biomassa_kg(800, 540.5))
print(biomassa_kg.__annotations__)
def gandakan(jumlah: int) -> int:
return jumlah * 2
# Python TIDAK memeriksa anotasi saat program jalan
print(gandakan(21))
print(gandakan("ab")) # bukan int, tetapi tetap jalan432.4
{'jumlah': <class 'int'>, 'bobot_g': <class 'float'>, 'return': <class 'float'>}
42
abab
jumlah: intartinya “jumlah diharapkan bilangan bulat”.-> floatdi ujung artinya “fungsi mengembalikan angka desimal”.- Variabel juga bisa diberi label:
umur_hari: int = 120. - Anotasi disimpan di
__annotations__sebagai catatan. - Python tidak memeriksanya saat berjalan.
gandakan("ab")bukanint, tetapi tetap jalan dan menghasilkanabab(teks dikali dua mengulang teks). Tidak ada galat. Pemeriksaan dilakukan alat terpisah, bukan Python.
Type hint adalah catatan, bukan penjaga pintu. Tanpa alat pemeriksa, salah tipe baru ketahuan saat program berjalan, atau malah tidak ketahuan seperti abab di atas. Untuk penjaga pintu saat berjalan, lihat Pydantic di Bab 34.
Tipe untuk koleksi, pilihan, dan fungsi
02_tipe_koleksi.py
# Bab 33 - Tipe untuk koleksi, pilihan, dan fungsi
from collections.abc import Callable, Iterator
def rata_rata(angka: list[float]) -> float:
return sum(angka) / len(angka)
def cari_kolam(data: dict[str, float], nama: str) -> float | None:
"""Mengembalikan bobot kolam, atau None bila tidak ada."""
return data.get(nama)
def titik(x: float, y: float) -> tuple[float, float]:
return (x, y)
def terapkan(fungsi: Callable[[float], float], nilai: list[float]) -> list[float]:
return [fungsi(n) for n in nilai]
def hitung_mundur(n: int) -> Iterator[int]:
while n > 0:
yield n
n -= 1
bobot = {"A": 540.5, "B": 498.0}
print(rata_rata([540.5, 498.0, 610.0]))
print(cari_kolam(bobot, "A"), cari_kolam(bobot, "Z"))
print(titik(1.5, 2.0))
print(terapkan(lambda g: g / 1000, [540.5, 498.0]))
print(list(hitung_mundur(3)))549.5
540.5 None
(1.5, 2.0)
[0.5405, 0.498]
[3, 2, 1]
| Penulisan | Artinya |
|---|---|
list[float] |
list yang isinya desimal |
dict[str, float] |
dict berkunci teks, bernilai desimal |
tuple[float, float] |
tuple dua desimal |
float \| None |
desimal atau None (artinya “boleh kosong”) |
Callable[[float], float] |
fungsi yang menerima satu desimal dan mengembalikan desimal |
Iterator[int] |
sesuatu yang bisa di-next() dan memberi bilangan bulat |
cari_kolam(bobot, "Z") mengembalikan None karena kolam Z tidak ada; itu sebabnya tipenya float | None. Penulisan float | None memakai tanda | (Python 3.10 ke atas). Callable dan Iterator diambil dari collections.abc. Perhatikan: hitung_mundur adalah generator (Bab 30), dan tipe yang pas untuk generator sederhana adalah Iterator[int].
Generics: tipe yang mengikuti isi
Fungsi pertama(daftar) bisa dipakai pada list desimal maupun list teks. Tipe hasilnya harus mengikuti: desimal bila diberi list desimal, teks bila diberi list teks. Itu tugas generics, dengan variabel tipe T.
03_generics.py
# Bab 33 - Generics: tipe yang mengikuti isi
from collections.abc import Sequence
def pertama[T](daftar: Sequence[T]) -> T:
"""Mengembalikan elemen pertama; tipenya sama dengan isi daftar."""
return daftar[0]
print(pertama([540.5, 498.0])) # T = float
print(pertama(["A", "B"])) # T = str
class Gudang[T]:
"""Tempat menyimpan barang bertipe sama."""
def __init__(self) -> None:
self.isi: list[T] = []
def simpan(self, barang: T) -> None:
self.isi.append(barang)
def ambil(self) -> T:
return self.isi.pop()
gudang_bobot = Gudang[float]()
gudang_bobot.simpan(540.5)
print(gudang_bobot.ambil())540.5
A
540.5
Sintaks def pertama[T](...) dan class Gudang[T] adalah cara penulisan yang diperkenalkan di Python 3.12 (sebelumnya memakai TypeVar dan Generic). Gudang[float]() membuat gudang khusus desimal. Alat pemeriksa lalu akan memprotes bila Anda menyimpan teks ke dalamnya.
TypedDict, Literal, dan Protocol
Data lapangan sering berbentuk dict. TypedDict menyatakan kunci apa saja yang ada dan tipenya. Literal membatasi pilihan ke nilai tertentu. Protocol menyatakan “apa pun yang punya ini” tanpa mewajibkan pewarisan (disebut duck typing: “bila berjalan seperti bebek dan bersuara seperti bebek, ia bebek”).
04_tipe_data_lapangan.py
# Bab 33 - TypedDict, Literal, dan Protocol
from dataclasses import dataclass
from typing import Literal, Protocol, TypedDict
Sektor = Literal["hutan", "kebun", "sawah", "kolam", "kandang"]
class Rekaman(TypedDict):
sektor: Sektor
unit: str
nilai: float
@dataclass
class Petak:
nama: str
luas_ha: float
class PunyaNama(Protocol):
"""Tipe 'bebek': apa pun yang punya atribut nama."""
nama: str
def sapa(objek: PunyaNama) -> str:
return "Halo, " + objek.nama
r: Rekaman = {"sektor": "kolam", "unit": "A", "nilai": 540.5}
print(r["sektor"], r["nilai"])
print(sapa(Petak("Blok A", 1.5)))kolam 540.5
Halo, Blok A
Rekaman adalah dict dengan tiga kunci, dan sektor hanya boleh salah satu dari lima nilai. sapa() menerima apa saja yang punya atribut nama: di sini Petak, sebuah dataclass (Bab 16), tanpa mewarisi apa pun. Saat berjalan, semuanya biasa dict dan objek biasa; labelnya hanya untuk pemeriksa.
Menjalankan mypy sungguhan
mypy adalah alat pemeriksa tipe. Skrip di bawah menulis berkas kecil yang sengaja salah, menjalankan mypy atasnya, dan menampilkan keluaran aslinya.
05_jalankan_mypy.py
# Bab 33 - Menjalankan mypy sungguhan pada berkas kecil
import subprocess
import sys
from pathlib import Path
folder = Path(__file__).parent / "_tmp"
folder.mkdir(exist_ok=True)
contoh = folder / "contoh_salah.py"
contoh.write_text('''def biomassa_kg(jumlah: int, bobot_g: float) -> float:
return jumlah * bobot_g / 1000
total: int = biomassa_kg(800, 540.5)
print(biomassa_kg("800", 540.5))
print(biomassa_kg(800))
nama: str = None
''', encoding="utf-8")
hasil = subprocess.run(
[sys.executable, "-m", "mypy", "--no-color-output", "--no-error-summary",
"--cache-dir", str(folder / ".mypy_cache"), "contoh_salah.py"],
cwd=folder, capture_output=True, text=True,
)
print(hasil.stdout.strip())
print("kode keluar mypy:", hasil.returncode)contoh_salah.py:5: error: Incompatible types in assignment (expression has type "float", variable has type "int") [assignment]
contoh_salah.py:6: error: Argument 1 to "biomassa_kg" has incompatible type "str"; expected "int" [arg-type]
contoh_salah.py:7: error: Missing positional argument "bobot_g" in call to "biomassa_kg" [call-arg]
contoh_salah.py:8: error: Incompatible types in assignment (expression has type "None", variable has type "str") [assignment]
kode keluar mypy: 1
Mypy menemukan empat kesalahan tanpa menjalankan berkas itu:
- Baris 5:
biomassa_kgmengembalikanfloat, tetapi disimpan ke variabel yang dijanjikanint. - Baris 6: argumen pertama berupa teks
"800", padahal diharapkanint. - Baris 7: argumen
bobot_gkurang. - Baris 8:
Nonediberikan ke variabel bertipestr.
Kode dalam kurung siku di ujung ([assignment], [arg-type], [call-arg]) adalah nama kategori galat. Kode keluar mypy 1 berarti “ada masalah”, yang berguna bila mypy dijalankan otomatis (misalnya di pemeriksaan sebelum commit, Bab 19 dan 20).
Mulailah dari yang kecil. Beri type hint pada fungsi yang paling sering dipakai, jalankan mypy, perbaiki, lalu perluas. Opsi --strict mypy lebih ketat (menuntut hampir semua fungsi diberi tipe); cocok bila proyek sudah rapi.
Pyright, alat pemeriksa lain
Selain mypy, ada pyright, pemeriksa tipe buatan Microsoft. Di halaman proyeknya, pyright digambarkan sebagai pemeriksa tipe statis untuk Python yang tersedia sebagai alat baris perintah dan ekstensi Visual Studio Code. Pyright tidak dijalankan di buku ini; cara memasangnya dan perbedaan perilakunya dengan mypy belum saya verifikasi di komputer penulis, jadi silakan lihat dokumentasinya sendiri bila ingin mencoba. Konsep yang Anda pelajari di sini (anotasi, generics, TypedDict) berlaku sama untuk keduanya.
Python 3.14: anotasi dievaluasi belakangan
Dulu, anotasi seperti x: Kelas langsung dievaluasi saat def dijalankan. Bila Kelas belum didefinisikan, terjadi galat, sehingga orang menulis "Kelas" (dalam tanda kutip) atau from __future__ import annotations. Sejak Python 3.14, anotasi disimpan dan dievaluasi hanya bila diperlukan (sumber: dokumentasi “What’s New in Python 3.14”, bagian anotasi tertunda). Akibatnya acuan ke nama yang belum ada tidak lagi memerlukan tanda kutip. Modul baru annotationlib membaca anotasi dalam tiga format.
06_anotasi_ditunda.py
# Bab 33 - Python 3.14: anotasi dievaluasi belakangan
from annotationlib import Format, get_annotations
def hitung(x: BelumDibuat) -> None: # nama ini BELUM ada
pass
print("fungsi berhasil dibuat tanpa galat")
print(get_annotations(hitung, format=Format.STRING))
acuan = get_annotations(hitung, format=Format.FORWARDREF)["x"]
print(type(acuan).__name__, acuan.__forward_arg__)
try:
get_annotations(hitung, format=Format.VALUE)
except NameError as e:
print("NameError:", e)
class BelumDibuat:
pass
print(get_annotations(hitung, format=Format.VALUE)) # sekarang namanya adafungsi berhasil dibuat tanpa galat
{'x': 'BelumDibuat', 'return': 'None'}
ForwardRef BelumDibuat
NameError: name 'BelumDibuat' is not defined
{'x': <class '__main__.BelumDibuat'>, 'return': None}
hitung(x: BelumDibuat)berhasil dibuat padahalBelumDibuatbelum ada. Pada Python lama ini langsung galatNameError.- Format
STRINGmenampilkan anotasi sebagai teks,FORWARDREFsebagai acuan (ForwardRef) bagi nama yang belum ada, danVALUEmengevaluasi nilainya sungguhan. - Format
VALUEsebelum kelas dibuat menghasilkanNameError. SetelahBelumDibuatdidefinisikan, formatVALUEberhasil dan mengembalikan kelasnya.
Skrip ini hanya berjalan di Python 3.14 ke atas, karena annotationlib baru ada di sana dan perilaku “fungsi dibuat walau nama belum ada” juga khas 3.14. Pembaca yang masih memakai Python lama sebaiknya tetap menulis acuan maju dalam tanda kutip. Kode from __future__ import annotations tetap dikenali dan mempertahankan perilaku lama.
Proyek Bersambung I4: memberi tipe pada kode proyek
Kita beri type hint pada fungsi pembaca rekaman dari Bab 30, lalu periksa dengan mypy mode ketat. Skrip menulis modul kecil lapangan.py ke _tmp/, menjalankan mypy, menyisipkan satu kesalahan sengaja, menjalankan mypy lagi, lalu menjalankan modulnya.
07_proyek_ketik_kode.py
# Bab 33 - Proyek bersambung I4: memberi tipe pada kode proyek, lalu diperiksa mypy
import subprocess
import sys
from pathlib import Path
folder = Path(__file__).parent / "_tmp"
folder.mkdir(exist_ok=True)
modul = folder / "lapangan.py"
modul.write_text('''from collections.abc import Iterable, Iterator
from typing import Literal, TypedDict, cast
Sektor = Literal["hutan", "kebun", "sawah", "kolam", "kandang"]
class Rekaman(TypedDict):
sektor: Sektor
unit: str
nilai: float
def baca(baris: Iterable[str]) -> Iterator[Rekaman]:
for b in baris:
sektor, unit, nilai = b.split(",")
yield {"sektor": cast(Sektor, sektor), "unit": unit, "nilai": float(nilai)}
def rata_rata(rekaman: Iterable[Rekaman]) -> float | None:
nilai = [r["nilai"] for r in rekaman]
return sum(nilai) / len(nilai) if nilai else None
print(rata_rata(baca(["kolam,A,540.5", "kolam,B,498.0"])))
print(rata_rata(baca([])))
''', encoding="utf-8")
def mypy(*opsi: str) -> None:
hasil = subprocess.run(
[sys.executable, "-m", "mypy", "--no-color-output", "--cache-dir", str(folder / ".mypy_cache"),
*opsi, "lapangan.py"],
cwd=folder, capture_output=True, text=True,
)
print(hasil.stdout.strip())
print("== mypy ==")
mypy("--strict")
print("== sengaja disisipi satu kesalahan ==")
modul.write_text(modul.read_text(encoding="utf-8") + "print(rata_rata([1.5, 2.5]))" + chr(10), encoding="utf-8")
mypy("--strict")
print("== menjalankan modul ==")
jalan = subprocess.run([sys.executable, "lapangan.py"], cwd=folder, capture_output=True, text=True)
print(jalan.stdout.strip())
print("galat saat jalan:", jalan.stderr.strip().splitlines()[-1])== mypy ==
Success: no issues found in 1 source file
== sengaja disisipi satu kesalahan ==
lapangan.py:26: error: List item 0 has incompatible type "float"; expected "Rekaman" [list-item]
lapangan.py:26: error: List item 1 has incompatible type "float"; expected "Rekaman" [list-item]
Found 2 errors in 1 file (checked 1 source file)
== menjalankan modul ==
519.25
None
galat saat jalan: TypeError: 'float' object is not subscriptable
Telusuri:
- Mypy
--strictmenyatakan modul bersih:Success: no issues found in 1 source file.cast(Sektor, sektor)memberi tahu mypy bahwa kita yakin teks itu salah satu nilaiSektor(mypy tidak bisa mengetahuinya dari teks bebas). Itu janji kita, bukan pemeriksaan; pemeriksaan sungguhan datang di Bab 34. - Setelah baris
print(rata_rata([1.5, 2.5]))disisipkan (seharusnya memberiRekaman, bukan angka), mypy melaporkan dua kesalahan, satu untuk tiap elemen list. Program belum dijalankan saat itu. - Bila modul tetap dijalankan, yang tercetak adalah 519.25 dan
Nonedari dua baris pertama, lalu program jatuh di baris sisipan denganTypeError: 'float' object is not subscriptable. Mypy menangkap kesalahan yang sama tanpa perlu menunggu program jatuh.
Latihan
- Beri type hint pada fungsi
rata_rata_bobot(bobot)yang menerimalist[float]dan mengembalikanfloat. - Tulis fungsi
cari_pohon(daftar: list[str], nama: str) -> int | Noneyang mengembalikan posisinamadalam daftar, atauNonebila tidak ada. - Ubah
05_jalankan_mypy.py: perbaiki empat kesalahan pada berkas contoh, jalankan lagi, dan lihat pesanSuccess. - Tantangan: buat
TypedDictbernamaTernak(kuncikandang: str,jenis: Literal["sapi", "kambing", "ayam"],bobot_kg: float), lalu fungsibobot_total(data: list[Ternak]) -> float. Periksa dengan mypy, termasuk satu data yangjenis-nya salah ketik.
Ringkasan Bab
- Type hint adalah label tipe pada variabel dan fungsi; Python tidak memeriksanya saat berjalan.
- Alat seperti mypy (dan pyright) memeriksa tipe sebelum program berjalan dan melaporkan ketidakcocokan.
- Tipe umum:
list[float],dict[str, float],float | None,Callable,Iterator; generics ([T]) membuat tipe mengikuti isi. TypedDict,Literal, danProtocolmenyatakan bentuk dict, pilihan terbatas, dan “apa pun yang punya ini”.- Python 3.14 menunda evaluasi anotasi;
annotationlibmembaca dalam formatVALUE,FORWARDREF, danSTRING. - Proyek I4: kode pembaca rekaman diberi tipe dan lulus
mypy --strict; kesalahan sisipan terlihat sebelum dijalankan.
Selanjutnya, di Bab 34: validasi data dan konfigurasi dengan Pydantic, penjaga pintu yang bekerja saat program berjalan.