Buka proyek Python yang sudah kamu tinggalkan enam bulan. Kamu menemukan fungsi process(data, config) dan sama sekali lupa data itu list, dict, atau objek khusus. Type hints ada untuk mencegah momen seperti ini. Python tidak pernah mewajibkan kamu menulis tipe, dan runtime-nya tetap tidak peduli. Tapi begitu proyek lewat seribu baris, anotasi tipe mengubah kode dari teka-teki jadi dokumentasi yang bisa dibaca mesin.

Kita mulai dari yang paling dasar, lalu naik ke pola yang sering muncul di proyek sungguhan: Optional, generik, Protocol, dan TypedDict. Di ujung, kamu akan bisa menjalankan mypy untuk menangkap bug tipe sebelum kode berjalan.

Dasar Anotasi

Anotasi paling sederhana adalah memberi tahu tipe sebuah variabel atau parameter. Sintaksnya pakai titik dua, dan tipe kembalian fungsi pakai tanda panah ->.

nama: str = "Budi"
umur: int = 27
tinggi: float = 175.5
aktif: bool = True


def sapa(nama: str) -> str:
    return f"Halo, {nama}"


def tambah(a: int, b: int) -> int:
    return a + b

Perhatikan: anotasi ini tidak mengubah apa pun saat program berjalan. tambah("a", "b") tetap dieksekusi dan menghasilkan "ab" — Python tidak peduli. Nilai type hints baru terasa saat kamu pakai editor modern (VS Code, PyCharm) atau alat seperti mypy untuk memeriksa kode secara statis.

List, Dict, dan Generik Bawaan

Kalau kamu tulis list saja, editor tidak tahu isinya apa. Sejak Python 3.9 kamu bisa menulis tipe elemen di dalam kurung siku langsung, tanpa impor tambahan.

python
def rata_rata(nilai: list[float]) -> float:
    return sum(nilai) / len(nilai)


def hitung_kata(kalimat: str) -> dict[str, int]:
    hasil: dict[str, int] = {}
    for kata in kalimat.split():
        hasil[kata] = hasil.get(kata, 0) + 1
    return hasil


koordinat: tuple[int, int] = (10, 20)
id_unik: set[int] = {1, 2, 3, 4}

Untuk Python 3.8 ke bawah, versi generiknya ada di modul typing: List[float], Dict[str, int], Tuple[int, int]. Kalau kamu masih mendukung versi lama, pakai itu. Mayoritas proyek baru sudah aman dengan sintaks kurung siku langsung.

Optional, Union, dan Operator |

Salah satu tipe yang paling sering kamu butuhkan adalah nilai yang bisa None. Pola lama memakai Optional[T], pola baru (Python 3.10+) cukup T | None. Dua-duanya valid, pilih satu dan konsisten di seluruh proyek.

python
def cari_pengguna(nama: str) -> dict | None:
    database = {"budi": {"umur": 27}, "siti": {"umur": 31}}
    return database.get(nama)


def bagi(a: int, b: int) -> float | None:
    if b == 0:
        return None
    return a / b


def format_uang(jumlah: int | float) -> str:
    return f"Rp {jumlah:,}"

Union biasa ditulis int | str. Hati-hati: kalau sebuah fungsi bisa melempar pengecualian, jangan dibungkus jadi | None — biarkan pengecualiannya naik. Tipe None khusus untuk nilai kosong yang memang valid secara bisnis.

Kalau kamu banyak memakai Optional dan list[...] dari modul typing, tambahkan from __future__ import annotations di baris pertama. Semua anotasi jadi string (lazy), jadi impor typing tidak dievaluasi saat runtime dan kode sedikit lebih cepat.

Type Alias

Ketika tipe yang sama muncul berulang — misalnya dict[str, int | float] untuk data sensor — beri nama. Alias membuat maksud kode langsung terbaca.

python
UserId = int
JsonData = dict[str, int | float | str | bool | None]
Koordinat = tuple[float, float]


def jarak(a: Koordinat, b: Koordinat) -> float:
    dx = a[0] - b[0]
    dy = a[1] - b[1]
    return (dx * dx + dy * dy) ** 0.5


def simpan(payload: JsonData) -> None:
    print(payload)

Sejak Python 3.12 ada kata kunci type yang lebih eksplisit: type UserId = int. Fungsinya sama, hanya lebih jelas bahwa kamu sedang mendefinisikan alias, bukan variabel. Saya tetap pakai tanda sama dengan biasa karena didukung lebih banyak versi.

Callable: Tipe untuk Fungsi

Kadang kamu menerima fungsi sebagai parameter — callback, handler, atau mapper. Anotasikan dengan Callable.

python
from collections.abc import Callable


def terapkan(operasi: Callable[[int, int], int], a: int, b: int) -> int:
    return operasi(a, b)


def kali(x: int, y: int) -> int:
    return x * y


print(terapkan(kali, 4, 5))  # 20

Argumen pertama Callable[[int, int], int] artinya: fungsi yang menerima dua int dan mengembalikan satu int. Impor dari collections.abc supaya kompatibel dengan isinstance.

Generik dengan TypeVar

Fungsi seperti terapkan kaku karena mengunci tipe int. Pakai TypeVar untuk membuat fungsi generik yang tetap menjaga hubungan tipe masuk-keluar.

python
from collections.abc import Sequence
from typing import TypeVar

T = TypeVar("T")


def pertama(daftar: Sequence[T]) -> T | None:
    return daftar[0] if daftar else None


print(pertama([1, 2, 3]))   # 1 (int)
print(pertama(["a", "b"]))  # 'a' (str)

Tanpa TypeVar, kamu terpaksa mengembalikan Any atau menulis versi terpisah untuk setiap tipe. Dengan TypeVar, mypy tahu bahwa pertama([1, 2, 3]) mengembalikan int, bukan sembarang objek.

Any adalah jalan pintas yang menular. Sekali fungsi mengembalikan Any, semua kode yang memakainya kehilangan pemeriksaan tipe secara diam-diam. Pakai Any hanya saat berhadapan dengan kode dinamis (misalnya hasil json.loads), lalu segera sempitkan tipenya.

Protocol: Duck Typing yang Terdefinisi

Python menghargai duck typing — objek dianggap valid kalau punya metode yang dibutuhkan, bukan karena mewarisi kelas tertentu. Protocol memformalkan pola ini untuk mypy.

python
from typing import Protocol


class BisaBunyi(Protocol):
    def bunyi(self) -> str: ...


class Kucing:
    def bunyi(self) -> str:
        return "meong"


class Alarm:
    def bunyi(self) -> str:
        return "bip bip"


def mainkan(objek: BisaBunyi) -> None:
    print(objek.bunyi())


mainkan(Kucing())  # meong
mainkan(Alarm())   # bip bip

Perhatikan Kucing dan Alarm tidak mewarisi BisaBunyi. mypy tetap menerimanya karena keduanya memenuhi struktur yang diminta. Ini berbeda dari ABC yang memaksa pewarisan eksplisit.

TypedDict untuk Struktur Dict

Dict Python sering dipakai menyimpan data berstruktur — respons API, baris database, konfigurasi. TypedDict memberi tipe pada kunci-kuncinya tanpa mengubah runtime sama sekali.

python
from typing import TypedDict


class Pengguna(TypedDict):
    nama: str
    umur: int
    email: str


def kirim_email(user: Pengguna) -> None:
    print(f"Kirim ke {user['email']}")


budi: Pengguna = {"nama": "Budi", "umur": 27, "email": "budi@contoh.com"}
kirim_email(budi)

Kalau kamu typo saat mengakses kunci, misalnya user['emial'], mypy akan menangkapnya. Ini keuntungan besar dibanding dict polos. Untuk validasi runtime yang lebih ketat, Pydantic lebih cocok — saya tulis panduannya di artikel Menguasai Pydantic.

Literal, Final, dan NewType

Tiga alat kecil ini menutup celah yang sering lolos dari pengecekan biasa. Literal membatasi nilai, Final mencegah reassignment, NewType memisahkan tipe yang kebetulan sama.

python
from typing import Final, Literal, NewType

Mode = Literal["cepat", "hemat", "seimbang"]


def atur_mode(mode: Mode) -> None:
    print(f"Mode aktif: {mode}")


atur_mode("cepat")   # ok
# atur_mode("acak")  # mypy error

MAX_RETRY: Final = 3

UserId = NewType("UserId", int)
OrderId = NewType("OrderId", int)


def proses(user: UserId) -> None:
    print(user)


u = UserId(42)
proses(u)
# proses(42)  # mypy error: int polos bukan UserId

NewType sering dianggap remeh, tapi mencegah bug nyata: tertukarnya user_id dan order_id yang sama-sama int. Di runtime, UserId benar-benar hanya int, jadi tidak ada overhead.

Menjalankan mypy

Semua anotasi di atas baru berguna kalau ada yang memeriksanya. Install mypy lalu jalankan.

bash
pip install mypy
mypy program.py

Kalau tidak ada output error, semua tipe konsisten. mypy juga membaca pyproject.toml untuk konfigurasi — misalnya strict = true yang menyalakan pemeriksaan paling ketat. Cara pakai mypy secara mendalam, termasuk adopsi bertahap ke proyek lama, saya bahas di artikel terpisah.

Type Hints di FastAPI dan Pydantic

Kalau kamu membangun API, type hints bukan lagi opsional — dia adalah inti framework-nya. FastAPI membaca anotasi fungsi untuk membuat validasi otomatis dan dokumentasi OpenAPI.

python
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    nama: str
    harga: float
    stok: int = 0


@app.post("/item")
def buat_item(item: Item) -> Item:
    return item

Di sini Item memakai anotasi yang sama persis dengan yang kamu pelajari barusan — hanya saja Pydantic memakainya untuk validasi saat runtime. Kalau kamu belum kenal FastAPI, mulai dari tutorial dasar FastAPI.

Mulailah dari satu file: tambahkan anotasi pada fungsi publik, jalankan mypy, lalu perluas ke seluruh modul. Type hints yang ditulis bertahap jauh lebih baik daripada refactor besar yang tidak pernah selesai. Kalau kamu sering membuat struktur data, pasangkan anotasi ini dengan dataclasses — kombinasi keduanya menghilangkan sebagian besar boilerplate kelas.