1. Pengenalan Type Hints
Type hints (atau type annotations) adalah fitur Python yang memungkinkan Anda mendeklarasikan tipe data untuk variabel, parameter fungsi, dan return value. Diperkenalkan di Python 3.5 melalui PEP 484, type hints membantu menulis kode yang lebih jelas, terdokumentasi, dan aman.
Penting untuk dipahami bahwa type hints tidak memaksa tipe data pada runtime seperti bahasa static-typed (Java, C++). Python tetap dynamic-typed β type hints hanya berfungsi sebagai dokumentasi dan bisa diperiksa oleh tools seperti mypy sebelum kode dijalankan.
Mengapa Type Hints Penting?
| Manfaat | Penjelasan |
|---|---|
| Dokumentasi | Tipe data jelas terlihat di signature fungsi |
| Autocomplete | IDE memberikan suggestions yang lebih akurat |
| Bug Prevention | Deteksi error sebelum runtime dengan mypy |
| Refactoring | Lebih aman dan mudah saat mengubah kode |
| Readability | Kode lebih mudah dipahami oleh developer lain |
| Tooling | Didukung oleh semua IDE modern (VS Code, PyCharm) |
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β TYPE HINTS WORKFLOW β β β β ββββββββββββ ββββββββββββ ββββββββββββ β β β Tulis βββββΊβ mypy βββββΊβ Runtime β β β β Kode + β β Check β β (Normal)β β β β Type β β β β β β β β Hints β β β Pass β β Python β β β β β β β Errorβ β ignore β β β ββββββββββββ ββββββββββββ ββββββββββββ β β β β Type hints = dokumentasi + static analysis β β BUKAN enforcement pada runtime β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
2. Sintaks Dasar Type Hints
Variable Annotations
# Type hints untuk variabel
nama: str = "Budi Santoso"
umur: int = 25
tinggi: float = 175.5
is_active: bool = True
# Tanpa nilai default
alamat: str # Hanya deklarasi, belum diisi
# Type hints TIDAK mengubah perilaku Python
x: int = "ini string" # Python tidak error, tapi mypy akan warning!
print(x) # "ini string" β tetap jalan
# Collection types dasar
angka: list[int] = [1, 2, 3, 4, 5]
pasangan: dict[str, int] = {"a": 1, "b": 2}
himpunan: set[str] = {"apel", "mangga"}
koordinat: tuple[float, float] = (3.14, 2.71)
campuran: tuple[int, str, float] = (1, "dua", 3.0)
# Type hint di class
class Pengguna:
nama: str
umur: int
email: str
def __init__(self, nama: str, umur: int, email: str) -> None:
self.nama = nama
self.umur = umur
self.email = email
Function Annotations
# Type hints untuk fungsi
# Sintaks: def nama(param: tipe) -> return_tipe:
def sapa(nama: str) -> str:
return f"Halo, {nama}!"
def tambah(a: int, b: int) -> int:
return a + b
def hitung_luas(panjang: float, lebar: float) -> float:
return panjang * lebar
# Tanpa return value β gunakan None
def cetak_pesan(pesan: str) -> None:
print(pesan)
# Default parameter dengan type hints
def buat_profil(
nama: str,
umur: int = 18,
kota: str = "Jakarta",
aktif: bool = True
) -> dict[str, str | int | bool]:
return {
"nama": nama,
"umur": umur,
"kota": kota,
"aktif": aktif
}
# Fungsi dengan multiple return types
def bagi(a: float, b: float) -> float | None:
if b == 0:
return None
return a / b
# Lambda dengan type hints (tidak didukung langsung)
# Gunakan variabel bertipe:
kali_dua: Callable[[int], int] = lambda x: x * 2
3. Modul Typing
Modul typing menyediakan tipe-tipe lanjutan yang tidak tersedia sebagai built-in types. Ini sangat penting untuk type hints yang lebih ekspresif.
from typing import (
Optional, Union, Any, NoReturn,
List, Dict, Tuple, Set, # Deprecated di Python 3.9+
Callable, Iterator, Generator,
TypeVar, Generic, ClassVar,
Final, Literal, TypeAlias
)
# === Optional β nilai bisa tipe atau None ===
def cari_user(user_id: int) -> Optional[str]:
"""Mengembalikan nama user atau None jika tidak ditemukan"""
database = {1: "Budi", 2: "Ani", 3: "Dimas"}
return database.get(user_id)
hasil = cari_user(1) # str atau None
hasil2 = cari_user(99) # None
# Optional[str] sama dengan Union[str, None]
# === Union β beberapa tipe yang mungkin ===
def proses_input(data: Union[str, int, float]) -> str:
"""Menerima input berbagai tipe"""
return str(data).upper()
# Python 3.10+ bisa pakai | operator:
def proses_input_modern(data: str | int | float) -> str:
return str(data).upper()
# === Any β tipe apapun diterima ===
def debug_print(value: Any) -> None:
"""Mencetak nilai apapun untuk debugging"""
print(f"[DEBUG] {type(value).__name__}: {value}")
# === Callable β fungsi sebagai parameter ===
from typing import Callable
def jalankan_dua_kali(
func: Callable[[int], int],
nilai: int
) -> int:
"""Menjalankan fungsi dua kali secara berurutan"""
return func(func(nilai))
hasil = jalankan_dua_kali(lambda x: x + 1, 5)
print(hasil) # 7 (5+1+1)
# Callable dengan signature lengkap
# Callable[[param_types], return_type]
handler: Callable[[str, int], bool]
# === Iterator dan Generator types ===
from typing import Iterator, Generator
def angka_gen(n: int) -> Iterator[int]:
for i in range(n):
yield i
def fibonacci() -> Generator[int, None, None]:
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# === Literal β nilai spesifik yang diterima ===
def set_mode(mode: Literal["read", "write", "append"]) -> None:
print(f"Mode: {mode}")
set_mode("read") # β
OK
set_mode("delete") # β mypy akan error!
# === Final β konstanta yang tidak boleh diubah ===
MAX_SIZE: Final = 100
APP_NAME: Final[str] = "BeebaneLabs"
# MAX_SIZE = 200 β mypy akan error!
4. Tipe Data Kompleks
from typing import Optional, Union
# === Nested collections ===
# List of dicts
siswa_list: list[dict[str, str | int]] = [
{"nama": "Budi", "umur": 25},
{"nama": "Ani", "umur": 22},
]
# Dict of lists
jadwal: dict[str, list[str]] = {
"senin": ["Matematika", "Fisika"],
"selasa": ["Kimia", "Biologi"],
}
# Dict of dicts
database: dict[str, dict[str, str | int]] = {
"user1": {"nama": "Budi", "umur": 25},
"user2": {"nama": "Ani", "umur": 22},
}
# Nested list (matrix)
matriks: list[list[int]] = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
]
# === TypedDict β dictionary dengan struktur tetap ===
from typing import TypedDict
class UserDict(TypedDict):
nama: str
umur: int
email: str
aktif: bool
# Sekarang dict harus punya key yang sesuai
user: UserDict = {
"nama": "Budi",
"umur": 25,
"email": "budi@email.com",
"aktif": True
}
# TypedDict dengan Optional keys
class ProductDict(TypedDict, total=False):
nama: str # Required (default)
harga: int # Required (default)
deskripsi: str # Optional (total=False)
rating: float # Optional (total=False)
# === TypeAlias ===
Vector = list[float]
Matrix = list[Vector]
UserId = int
JSON = dict[str, Union[str, int, float, bool, None, list, dict]]
def dot_product(v1: Vector, v2: Vector) -> float:
return sum(a * b for a, b in zip(v1, v2))
def get_user(user_id: UserId) -> Optional[dict]:
pass
# === Overload β beberapa signature untuk fungsi yang sama ===
from typing import overload
@overload
def process(value: int) -> int: ...
@overload
def process(value: str) -> str: ...
@overload
def process(value: list[int]) -> list[int]: ...
def process(value):
if isinstance(value, int):
return value * 2
elif isinstance(value, str):
return value.upper()
elif isinstance(value, list):
return sorted(value)
5. Generics
Generics memungkinkan Anda menulis kode yang bekerja dengan berbagai tipe data sambil tetap mempertahankan type safety. Ini sangat berguna untuk data structures dan utility functions.
from typing import TypeVar, Generic
# TypeVar β tipe placeholder
T = TypeVar('T') # Tipe apapun
K = TypeVar('K') # Untuk key
V = TypeVar('V') # Untuk value
N = TypeVar('N', int, float) # Hanya int atau float (constrained)
# === Generic Function ===
def first_element(items: list[T]) -> T | None:
"""Mengembalikan elemen pertama dari list"""
if items:
return items[0]
return None
# Type checker tahu hasilnya int karena input list[int]
angka = first_element([1, 2, 3]) # int | None
teks = first_element(["a", "b"]) # str | None
# === Generic Class ===
class Stack(Generic[T]):
"""Stack generik yang bisa menyimpan tipe apapun"""
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
if not self._items:
raise IndexError("Stack kosong!")
return self._items.pop()
def peek(self) -> T | None:
if self._items:
return self._items[-1]
return None
def is_empty(self) -> bool:
return len(self._items) == 0
def size(self) -> int:
return len(self._items)
def __repr__(self) -> str:
return f"Stack({self._items})"
# Stack dengan tipe spesifik
int_stack: Stack[int] = Stack()
int_stack.push(1)
int_stack.push(2)
int_stack.push(3)
print(int_stack) # Stack([1, 2, 3])
print(int_stack.pop()) # 3
str_stack: Stack[str] = Stack()
str_stack.push("hello")
str_stack.push("world")
# === Generic dengan multiple type parameters ===
class Pair(Generic[K, V]):
"""Pasangan key-value generik"""
def __init__(self, key: K, value: V) -> None:
self.key = key
self.value = value
def __repr__(self) -> str:
return f"Pair({self.key!r}, {self.value!r})"
def swap(self) -> 'Pair[V, K]':
return Pair(self.value, self.key)
p = Pair("nama", 25)
print(p) # Pair('nama', 25)
print(p.swap()) # Pair(25, 'nama')
# === Generic dengan TypeVar constraints ===
def double(x: N) -> N:
"""Hanya menerima int atau float"""
return x * 2 # type: ignore
print(double(5)) # 10
print(double(3.14)) # 6.28
# double("hi") β mypy error!
6. Protocols (Structural Subtyping)
Protocols memungkinkan Anda mendefinisikan interface berdasarkan struktur (method dan attribute yang dimiliki), bukan berdasarkan inheritance. Ini dikenal sebagai structural subtyping atau duck typing yang ter-typed.
from typing import Protocol, runtime_checkable
# === Basic Protocol ===
class Drawable(Protocol):
"""Protocol untuk objek yang bisa digambar"""
def draw(self) -> str: ...
class Printable(Protocol):
"""Protocol untuk objek yang bisa dicetak"""
def print(self) -> None: ...
# Class yang memenuhi protocol TANPA perlu inherit
class Circle:
def __init__(self, radius: float) -> None:
self.radius = radius
def draw(self) -> str:
return f"Drawing circle with radius {self.radius}"
class Square:
def __init__(self, side: float) -> None:
self.side = side
def draw(self) -> str:
return f"Drawing square with side {self.side}"
# Fungsi yang menerima apapun yang punya method draw()
def render(obj: Drawable) -> None:
print(obj.draw())
render(Circle(5)) # β
OK: Circle punya draw()
render(Square(3)) # β
OK: Square punya draw()
# render("hello") # β mypy error: str tidak punya draw()
# === Protocol dengan attributes ===
class HasName(Protocol):
name: str
age: int
class Person:
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
class Animal:
def __init__(self, name: str, age: int, species: str) -> None:
self.name = name
self.age = age
self.species = species
def greet(entity: HasName) -> str:
return f"Hello, {entity.name}! You are {entity.age} years old."
print(greet(Person("Budi", 25))) # β
OK
print(greet(Animal("Kucing", 3, "Persia"))) # β
OK
# === runtime_checkable Protocol ===
@runtime_checkable
class Closeable(Protocol):
def close(self) -> None: ...
# Bisa di-check pada runtime!
import io
f = io.StringIO("hello")
print(isinstance(f, Closeable)) # True (StringIO punya close())
# === Protocol dengan method signatures ===
class Serializer(Protocol):
def serialize(self, data: dict) -> str: ...
def deserialize(self, text: str) -> dict: ...
class JSONSerializer:
def serialize(self, data: dict) -> str:
import json
return json.dumps(data)
def deserialize(self, text: str) -> dict:
import json
return json.loads(text)
class CSVSerializer:
def serialize(self, data: dict) -> str:
return ",".join(f"{k}={v}" for k, v in data.items())
def deserialize(self, text: str) -> dict:
return dict(item.split("=") for item in text.split(","))
def save_data(serializer: Serializer, data: dict) -> str:
return serializer.serialize(data)
print(save_data(JSONSerializer(), {"a": 1, "b": 2})) # {"a": 1, "b": 2}
print(save_data(CSVSerializer(), {"a": 1, "b": 2})) # a=1,b=2
7. Static Type Checking dengan mypy
mypy adalah static type checker untuk Python. Ia memeriksa type hints dalam kode Anda dan mendeteksi kesalahan tipe tanpa menjalankan program. mypy adalah tool yang paling populer dan menjadi standar untuk type checking di Python.
# Instal mypy pip install mypy # Jalankan type checking pada file mypy myfile.py # Jalankan pada seluruh project mypy src/ # Dengan strict mode (lebih ketat) mypy --strict myfile.py # Contoh konfigurasi di pyproject.toml # [tool.mypy] # python_version = "3.12" # warn_return_any = true # warn_unused_configs = true # disallow_untyped_defs = true # check_untyped_defs = true
# === Contoh error yang bisa dideteksi mypy ===
# Error 1: Tipe mismatch
def tambah(a: int, b: int) -> int:
return a + b
hasil = tambah("hello", "world") # β mypy: Argument 1 has incompatible type "str"; expected "int"
# Error 2: Return type mismatch
def get_nama() -> str:
return 42 # β mypy: Incompatible return value type (got "int", expected "str")
# Error 3: Optional handling
from typing import Optional
def cari(id: int) -> Optional[str]:
if id == 1:
return "Budi"
return None
nama = cari(1)
print(nama.upper()) # β mypy: Item "None" of "Optional[str]" has no attribute "upper"
# Cara yang benar:
nama = cari(1)
if nama is not None:
print(nama.upper()) # β
OK: Type narrowed to str
# Atau gunakan assert:
nama = cari(1)
assert nama is not None
print(nama.upper()) # β
OK
# Error 4: Missing type annotation
def fungsi_tanpa_tipe(data): # β mypy (strict): Function is missing a type annotation
return data * 2
# Fix: tambahkan type hints
def fungsi_dengan_tipe(data: int) -> int:
return data * 2
# === Mengatasi error mypy ===
# 1. Type ignore (dengan alasan yang jelas)
x = some_function() # type: ignore[attr-defined] # noqa
# 2. Cast
from typing import cast
nilai = cast(int, input_data) # Memberitahu mypy bahwa ini int
# 3. TypeGuard (Python 3.10+)
from typing import TypeGuard
def is_string_list(val: list[object]) -> TypeGuard[list[str]]:
return all(isinstance(x, str) for x in val)
def process(data: list[object]) -> None:
if is_string_list(data):
print(", ".join(data)) # mypy tahu ini list[str] di sini
8. Type Hints Modern (Python 3.10+)
Python 3.10+ membawa banyak penyederhanaan sintaks untuk type hints, membuatnya lebih mudah dibaca dan ditulis.
# === Python 3.10+ Union dengan | operator ===
# Sebelum 3.10:
from typing import Union, Optional
def proses_lama(data: Union[str, int]) -> Optional[str]:
pass
# Python 3.10+: jauh lebih singkat!
def proses_baru(data: str | int) -> str | None:
pass
# Bisa digunakan di mana saja
nilai: int | float = 42
hasil: str | None = cari_sesuatu()
# === match-case dengan type hints (Python 3.10+) ===
def handle(value: str | int | list[int]) -> str:
match value:
case str():
return f"String: {value}"
case int():
return f"Int: {value}"
case list():
return f"List: {value}"
case _:
return "Unknown"
# === Type Hints di assignment (Python 3.6+, lebih baik 3.10+) ===
# Builtin generics (tidak perlu import List, Dict, dll)
angka: list[int] = [1, 2, 3] # Bukan List[int]
data: dict[str, int] = {"a": 1} # Bukan Dict[str, int]
koordinat: tuple[int, int] = (1, 2) # Bukan Tuple[int, int]
himpunan: set[str] = {"a", "b"} # Bukan Set[str]
# === ParamSpec (Python 3.10+) ===
from typing import ParamSpec, TypeVar
P = ParamSpec('P')
R = TypeVar('R')
def log_decorator(func: Callable[P, R]) -> Callable[P, R]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
# === TypeVarTuple (Python 3.11+) β variadic generics ===
from typing import TypeVarTuple
Ts = TypeVarTuple('Ts')
# === dataclass_transform (Python 3.11+) ===
# Memungkinkan custom dataclass-like classes mendapat mypy support
# === override decorator (Python 3.12+) ===
from typing import override
class Base:
def method(self) -> str:
return "base"
class Child(Base):
@override
def method(self) -> str: # mypy cek bahwa Base.method ada
return "child"
9. Studi Kasus Praktis
API Client dengan Type Hints
from typing import TypedDict, Optional, Literal
from dataclasses import dataclass
# === Definisikan tipe data ===
class UserResponse(TypedDict):
id: int
name: str
email: str
role: Literal["admin", "user", "viewer"]
active: bool
@dataclass
class ApiResponse:
status: int
data: Optional[dict | list]
message: str
@property
def is_success(self) -> bool:
return 200 <= self.status < 300
@dataclass
class ApiClient:
base_url: str
api_key: str
def get_user(self, user_id: int) -> Optional[UserResponse]:
"""Mengambil data user dari API"""
# Simulasi API call
users: dict[int, UserResponse] = {
1: {"id": 1, "name": "Budi", "email": "budi@mail.com",
"role": "admin", "active": True},
2: {"id": 2, "name": "Ani", "email": "ani@mail.com",
"role": "user", "active": True},
}
return users.get(user_id)
def search_users(
self,
query: str,
role: Optional[Literal["admin", "user"]] = None,
limit: int = 10
) -> list[UserResponse]:
"""Mencari user berdasarkan query"""
all_users: list[UserResponse] = [
{"id": 1, "name": "Budi", "email": "budi@mail.com",
"role": "admin", "active": True},
{"id": 2, "name": "Ani", "email": "ani@mail.com",
"role": "user", "active": True},
]
result = [u for u in all_users if query.lower() in u["name"].lower()]
if role:
result = [u for u in result if u["role"] == role]
return result[:limit]
# Penggunaan
client = ApiClient("https://api.example.com", "key123")
user = client.get_user(1)
if user:
print(f"User: {user['name']} ({user['role']})")
results = client.search_users("bu", role="admin")
for u in results:
print(f" - {u['name']} ({u['email']})")
Repository Pattern dengan Generics
from typing import TypeVar, Generic, Optional, Protocol
from dataclasses import dataclass, field
class Entity(Protocol):
@property
def id(self) -> int: ...
T = TypeVar('T', bound=Entity)
@dataclass
class User:
id: int
name: str
email: str
@dataclass
class Product:
id: int
name: str
price: float
class Repository(Generic[T]):
"""Generic repository pattern"""
def __init__(self) -> None:
self._items: dict[int, T] = {}
self._next_id: int = 1
def add(self, item: T) -> T:
self._items[item.id] = item
return item
def get_by_id(self, item_id: int) -> Optional[T]:
return self._items.get(item_id)
def get_all(self) -> list[T]:
return list(self._items.values())
def delete(self, item_id: int) -> bool:
if item_id in self._items:
del self._items[item_id]
return True
return False
def count(self) -> int:
return len(self._items)
# Type-safe repositories
user_repo: Repository[User] = Repository()
product_repo: Repository[Product] = Repository()
user_repo.add(User(1, "Budi", "budi@mail.com"))
user_repo.add(User(2, "Ani", "ani@mail.com"))
product_repo.add(Product(1, "Laptop", 15000000))
product_repo.add(Product(2, "Mouse", 150000))
# mypy tahu hasilnya User
user = user_repo.get_by_id(1)
if user:
print(f"User: {user.name}") # β
autocomplete untuk .name
# mypy tahu hasilnya Product
product = product_repo.get_by_id(1)
if product:
print(f"Product: {product.price}") # β
autocomplete untuk .price
10. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang Type Hints: