Memahami Type Hinting di Python: Static Analysis dengan mypy dan Validasi Runtime dengan Pydantic

Lhuqita Fazry
Python Type Hinting mypy Pydantic Static Analysis
Memahami Type Hinting di Python: Static Analysis dengan mypy dan Validasi Runtime dengan Pydantic

Mengapa Type Hinting Dibutuhkan pada Codebase Python yang Bertumbuh

Python tetap bersifat dynamically typed. Annotation pada def total(tagihan: int) -> int tidak mengubah cara interpreter mengeksekusi fungsi. Fungsi tetap menerima objek apa pun, dan konversi tipe tidak terjadi secara otomatis. Konsekuensinya, kesalahan tipe baru terlihat ketika baris tersebut benar-benar dijalankan.

Pada codebase kecil, kesalahan seperti ini mudah dilacak. Pada codebase yang memiliki puluhan modul, argumen yang salah tipe sering berasal dari modul lain dan baru terdeteksi di produksi. Annotation berfungsi sebagai kontrak antar-modul. Manusia membaca maksud fungsi, sementara tool seperti mypy memeriksa apakah pemanggil mematuhi kontrak tersebut.

Pertahanan yang sehat terdiri dari dua lapisan. Static analysis memeriksa kode sebelum dijalankan, sedangkan validasi runtime memeriksa data yang masuk dari luar sistem. mypy bekerja pada lapisan pertama, Pydantic pada lapisan kedua. Keduanya menyelesaikan masalah yang berbeda dan tidak saling menggantikan.

python
def total(tagihan, diskon):
    return tagihan - diskon

def total_aman(tagihan: int, diskon: int) -> int:
    return tagihan - diskon

for fungsi in (total, total_aman):
    try:
        fungsi("50000", 5000)
    except TypeError as error:
        print(f"{fungsi.__name__}: TypeError: {error}")

print(total_aman(50000, 5000))

Output:

text
total: TypeError: unsupported operand type(s) for -: 'str' and 'int'
total_aman: TypeError: unsupported operand type(s) for -: 'str' and 'int'
45000

Kedua fungsi gagal pada titik yang sama. Annotation pada total_aman tidak memaksa Python mengubah "50000" menjadi angka, sehingga TypeError tetap muncul saat runtime. Perbedaannya, mypy dapat menemukan pemanggilan yang salah sebelum kode dieksekusi. Inilah pemisahan peran yang perlu kita pegang sepanjang artikel ini.

Menyusun Annotation untuk Function, Collection, dan Struktur Data

Annotation pada parameter dan return value memakai sintaks yang sama dengan assignment. def hitung(angka: int) -> str: menyatakan input dan output, sedangkan -> None menandai fungsi yang tidak mengembalikan nilai. Sejak Python 3.9, generic bawaan tidak lagi membutuhkan typing.List atau typing.Dict. Kita cukup menulis list[str], dict[str, int], set[str], atau tuple[int, ...] untuk tuple dengan panjang bebas.

Hierarki tipe standar Python 3 yang menjadi dasar penulisan annotation

Gambar: Hierarki tipe standar Python 3, tempat `int`, `str`, `bool`, `list`, `tuple`, `dict`, dan `set` menjadi tipe bawaan yang paling sering muncul pada annotation — Sumber: Wikimedia Commons

Lisensi: CC BY-SA 4.0 — Максим Пе

Union modern memakai operator |. Tipe int | None menyatakan nilai yang boleh kosong, dan Optional[int] adalah padanan lama untuk maksud yang sama. Untuk nilai yang hanya boleh berasal dari daftar tertentu, Literal["admin", "member"] lebih tepat daripada str. Callable[[int, int], int] mendeskripsikan fungsi sebagai argumen, sementara TypeVar menghubungkan tipe input dengan tipe output pada fungsi generic.

TypedDict memberi bentuk pada dictionary, dan dataclass memberi bentuk pada objek domain. Keduanya berguna saat data melewati beberapa fungsi dengan struktur yang tetap. Annotation yang terlalu detail pada struktur sementara justru menambah noise. Sebaliknya, Any sebagai jalan pintas mematikan manfaat pemeriksaan karena semua operasi pada nilai tersebut dianggap valid.

python
from dataclasses import dataclass
from typing import Literal, TypedDict

class UserPayload(TypedDict):
    id: int
    name: str
    role: Literal["admin", "member"]

@dataclass(frozen=True)
class Account:
    id: int
    name: str
    role: str

    @property
    def label(self) -> str:
        return f"{self.name} ({self.role})"

def build_account(payload: UserPayload) -> Account:
    return Account(
        id=payload["id"],
        name=payload["name"],
        role=payload["role"],
    )

payload: UserPayload = {"id": 7, "name": "Rani", "role": "admin"}
account = build_account(payload)
print(account)
print(account.label)

Output:

text
Account(id=7, name='Rani', role='admin')
Rani (admin)

UserPayload mendokumentasikan kunci yang wajib ada, dan Account menjadi objek domain yang immutable karena frozen=True. Perhatikan bahwa annotation tidak menambah validasi. Dictionary dengan kunci yang salah tetap bisa dibuat saat runtime. Pemeriksaan bentuk data seperti ini adalah tugas mypy untuk kode internal, dan tugas Pydantic untuk data yang berasal dari luar.

Menjalankan mypy dan Membaca Temuan Static Analysis

Data Science with Python
Data Science • Beginner

Data Science with Python

Master the art of data analysis, visualization, and predictive modeling.

Daftar

mypy dijalankan dari terminal dengan argumen berupa file atau direktori package. Perintah mypy app/ memeriksa seluruh modul di dalam folder, sedangkan mypy src/main.py membatasi pemeriksaan pada satu file. Format temuan selalu mengikuti pola file:baris: error: pesan [kode], sehingga kita bisa melacak masalah langsung ke baris sumber.

Pesan seperti Argument 1 has incompatible type "str"; expected "int" berarti tipe argumen tidak sesuai dengan annotation parameter. Mencari nama fungsi pada baris tersebut biasanya cukup untuk menemukan pemanggil yang bermasalah. Flag --strict mengaktifkan sekumpulan pemeriksaan tambahan, termasuk disallow_untyped_defs dan warn_return_any. Kedua opsi ini memaksa setiap fungsi memiliki annotation dan mencegah nilai Any bocor ke return value.

Konfigurasi permanen sebaiknya disimpan di pyproject.toml atau mypy.ini agar hasil di lokal sama dengan hasil di CI. Pengecualian memakai # type: ignore[kode] dengan alasan yang jelas, misalnya library eksternal yang belum menyediakan stub. Komentar tanpa kode error yang spesifik akan menyembunyikan masalah baru. Integrasi ke pre-commit atau pipeline CI menjadikan pemeriksaan ini gate sebelum merge.

python
!pip install mypy

import subprocess
import sys
from pathlib import Path

source = '''
def format_score(score: int) -> str:
    pass

def show_score(label: str) -> None:
    print(label)

show_score(format_score("90"))
record = {"user": "budi", "score": 90}
print(record.missing_field)
'''

fixture = Path("fixture_types.py")
fixture.write_text(source, encoding="utf-8")

result = subprocess.run(
    [sys.executable, "-m", "mypy", "--strict", "fixture_types.py"],
    capture_output=True,
    text=True,
)
print(result.stdout)

fixture.unlink()

Output:

text
fixture_types.py:2: error: Missing return statement  [empty-body]
fixture_types.py:8: error: Argument 1 to "format_score" has incompatible type "str"; expected "int"  [arg-type]
fixture_types.py:10: error: "dict[str, object]" has no attribute "missing_field"  [attr-defined]
Found 3 errors in 1 file (checked 1 source file)

Script di atas menulis fixture sementara lewat pathlib, menjalankan mypy sebagai subprocess, lalu menghapus file tersebut. Tiga temuan yang muncul mewakili kesalahan yang paling sering terjadi: fungsi dengan deklarasi -> str tetapi tidak mengembalikan nilai, argumen dengan tipe yang salah, dan akses atribut yang tidak ada pada dictionary. Konfigurasi minimum untuk proyek dapat disimpan seperti berikut.

toml
[tool.mypy]
python_version = "3.10"
strict = true
warn_return_any = true
disallow_untyped_defs = true

Memvalidasi Data Eksternal dengan Pydantic

mypy tidak dapat memeriksa isi JSON, body request HTTP, atau environment variable. Semua nilai tersebut baru diketahui saat program berjalan, dan annotation pada fungsi tidak akan menyentuhnya. Di sinilah validasi runtime dibutuhkan. Pydantic menyediakan BaseModel sebagai schema yang mem-parsing input, mengonversi tipe yang kompatibel, dan menolak data yang tidak sesuai.

Field menambahkan constraint pada atribut, seperti min_length, ge, le, dan nilai default. model_validate menerima dictionary, sedangkan model_validate_json menerima string JSON. Ketika input tidak valid, ValidationError berisi daftar error dengan loc yang menunjukkan field bermasalah dan msg yang menjelaskan penyebabnya. Setelah data bersih, model_dump dan model_dump_json menghasilkan representasi siap dikirim ke layer berikutnya.

dataclass tetap pilihan yang lebih ringan untuk objek internal yang dibuat oleh kode kita sendiri. BaseModel dipilih ketika data menyeberangi batas sistem dan perlu diperiksa. Perbedaan ini mencerminkan dua titik pemeriksaan: saat kode ditulis dan saat data masuk ke sistem.

python
!pip install pydantic[email]

from pydantic import BaseModel, EmailStr, Field, ValidationError

class UserRegistration(BaseModel):
    name: str = Field(min_length=3)
    email: EmailStr
    age: int = Field(ge=18, le=120)

valid = UserRegistration.model_validate(
    {"name": "Rani", "email": "[email protected]", "age": 24}
)
print(valid)
print(valid.model_dump_json())

try:
    UserRegistration.model_validate(
        {"name": "Rani", "email": "rani@", "age": 16}
    )
except ValidationError as error:
    for issue in error.errors():
        print(issue["loc"], "->", issue["msg"])

Output:

text
name='Rani' email='[email protected]' age=24
{"name":"Rani","email":"[email protected]","age":24}
('email',) -> value is not a valid email address: There must be something after the @-sign.
('age',) -> Input should be greater than or equal to 18

Input pertama lolos karena semua constraint terpenuhi, dan hasil model_dump_json siap dikirim sebagai response. Input kedua menghasilkan dua error yang menunjuk field email dan age. Pesan error seperti ini bisa langsung diteruskan ke client tanpa parsing tambahan.

Menggabungkan Static Analysis dan Validasi Runtime dalam Satu Alur Kerja

Pembagian tanggung jawabnya jelas. mypy menjaga konsistensi internal kode, sedangkan Pydantic menjaga batas sistem. Data eksternal masuk lewat model, diteruskan ke function yang memiliki annotation, lalu keluar sebagai model atau TypedDict. Alur ini mencegah data mentah menyebar ke seluruh aplikasi.

Aktifkan plugin pydantic.mypy pada konfigurasi agar mypy memahami BaseModel dan menghasilkan pemeriksaan yang lebih akurat. Satu model sebaiknya menjadi sumber kebenaran untuk validasi sekaligus annotation. Mendefinisikan ulang schema yang sama di dua tempat akan menciptakan dua versi aturan yang cepat atau lambat berbeda.

Kesalahan yang sering terjadi adalah memanggil model_construct untuk melewati validasi. Method ini memang lebih cepat, tetapi semua jaminan tipe hilang tanpa peringatan. Sebelum merge, pastikan mypy bersih, test validasi lulus, dan tidak ada Any tanpa alasan tertulis.

python
!pip install mypy pydantic

import subprocess
import sys
from pathlib import Path

from pydantic import BaseModel, Field

class Order(BaseModel):
    order_id: int
    total: int = Field(ge=0)

def format_order(order: Order) -> str:
    return f"Order #{order.order_id}: Rp{order.total:,}"

def process(raw: dict[str, object]) -> str:
    return format_order(Order.model_validate(raw))

print(process({"order_id": 12, "total": 250000}))

module = Path("checked_order.py")
module.write_text(
    "from pydantic import BaseModel, Field\n\n"
    "class Order(BaseModel):\n"
    "    order_id: int\n"
    "    total: int = Field(ge=0)\n\n"
    "def format_order(order: Order) -> str:\n"
    "    return f'Order #{order.order_id}: Rp{order.total:,}'\n",
    encoding="utf-8",
)
config = Path("mypy.ini")
config.write_text(
    "[mypy]\n"
    "strict = True\n"
    "plugins = pydantic.mypy\n",
    encoding="utf-8",
)
result = subprocess.run(
    [sys.executable, "-m", "mypy", "--config-file", str(config), str(module)],
    capture_output=True,
    text=True,
)
print(result.stdout.strip())
module.unlink()
config.unlink()

Output:

text
Order #12: Rp250,000
Success: no issues found in 1 source file

Function process menerima dictionary mentah, tetapi langsung mengubahnya menjadi Order sebelum menyentuh logika bisnis. format_order hanya menerima Order, sehingga mypy dapat memastikan tidak ada atribut yang salah. Konfigurasi mypy.ini di atas mengaktifkan plugin dan strict mode dalam satu tempat.

Batasan Type Hinting dan Praktik yang Perlu Dihindari

Annotation tidak menjamin validitas data. mypy adalah pemeriksa statis, bukan runtime guard, dan annotation tidak dieksekusi saat program berjalan. Nilai yang dibuat lewat cast, getattr, atau deserialisasi manual tetap bisa melanggar tipe yang dideklarasikan. Karena itu, validasi runtime tetap wajib pada batas sistem.

Hindari Any dan cast yang berlebihan. Keduanya mematikan pemeriksaan pada titik tersebut, dan efeknya menyebar ke fungsi lain yang memakai nilai hasilnya. Jika tipe sulit dinyatakan, mulai dari struktur data yang lebih sederhana atau pecah function menjadi bagian yang lebih kecil.

Ada biaya yang perlu dihitung. Import Pydantic menambah waktu startup, dan menulis annotation menuntut disiplin. Untuk script sekali pakai, notebook eksploratif, atau prototipe awal, annotation lengkap sering tidak sepadan. Adopsi bertahap lebih realistis: mulai dari modul inti, aktifkan strictness per direktori, lalu perluas cakupannya.

Ringkasnya, mypy cukup untuk kode internal yang kita kendalikan sepenuhnya. Validasi runtime wajib ada begitu data datang dari luar, termasuk API, file, dan environment variable. Kombinasi keduanya memberi umpan balik cepat saat menulis kode dan perlindungan nyata saat program berjalan.

Type hinting, mypy, dan Pydantic adalah keterampilan yang paling cepat dikuasai lewat praktik langsung pada proyek nyata. Python Bootcamp Rumah Coding membahas ketiganya bersama mentor, mulai dari menyusun annotation, membaca temuan mypy, sampai merancang validasi data untuk aplikasi produksi.

Kursus Terkait

E-commerce Sales Dashboard
Kursus Premium Data Science

Data Science with Python

Master the art of data analysis, visualization, and predictive modeling.

Proyek Akhir

E-commerce Sales Dashboard

  • Data Cleaning Pipeline
  • Interactive Charts
  • Sales Forecasting Model
7 Weeks Beginner
Lihat Detail Kursus
GreenGuard: Intelligent Plant Disease Diagnosis Web App
Kursus Premium Machine Learning

Deep Learning Bootcamp

A beginner-friendly, highly interactive bootcamp designed to take you from foundational concepts to deploying real-world Artificial Intelligence applications. Through a completely project-based approach, you will master the core of Deep Learning, Artificial Neural Networks, and Computer Vision using Python and TensorFlow, ultimately building a professional-grade AI web application for your portfolio.

Proyek Akhir

GreenGuard: Intelligent Plant Disease Diagnosis Web App

  • Interactive Image Upload UI: A clean, user-friendly interface built with Streamlit that supports drag-and-drop image uploads directly from a computer or mobile phone.
  • Real-Time AI Inference: Utilizes a lightweight, optimized CNN model (like MobileNetV2) to process the image and return a diagnosis in seconds without heavy server load.
  • Confidence Scoring Dashboard: Visually displays the model's prediction probability (e.g., "95% confident this is Tomato Late Blight") using interactive progress bars or charts.
7 Weeks Intermediate
Lihat Detail Kursus
Domain-Specific AI Knowledge Assistant
Kursus Premium Machine Learning

LLM Bootcamp

This project-based bootcamp is designed for beginners to dive practically into the world of Large Language Models (LLMs). Through hands-on building, you will learn how to interact with top-tier AI APIs, master prompt engineering, orchestrate complex workflows using LangChain, and implement Retrieval-Augmented Generation (RAG) to query your own documents. By the end of this course, you will have the skills to build, test, and deploy a fully functional, custom AI web application.

Proyek Akhir

Domain-Specific AI Knowledge Assistant

  • Dynamic Document Processing: A sidebar interface allowing users to upload new PDF or TXT files, which the app automatically chunks, embeds, and stores in the vector database.
  • Context-Aware Chat UI: A modern chat interface built with Streamlit that maintains conversation history, allowing users to ask follow-up questions naturally.
  • Strict Guardrails (Anti-Hallucination): System instructions designed so the AI politely declines to answer questions that fall outside the context of the uploaded documents.
7 Weeks Beginner
Lihat Detail Kursus

Artikel Terkait