Mencoba Kembali Python Typehint

Mencoba Kembali Python Typehint
Photo by Chris Ried / Unsplash

Seperti yang saya tulis di tulisan saya sebelumnya, saat ini saya lebih banyak menulis kode dengan Go dan akhirnya berhasil mendapatkan feel menulis dengan type. Karena hal itu, akhirnya saya ingin mencoba kembali hal yang dulu sempat dicoba tapi berhenti karena menyebalkan — hal itu adalah Python dengan typehint.

Typehint dan Ekspektasi

Sebelum melangkah lebih jauh, saya memang harus mengatur ekspektasi, karena bagaimanapun secara natural Python bukanlah static type, dan typehint hanya sebagai petunjuk atau paling mentok ya himbauan.

Karena tidak ada paksaan dalam penggunaan type di Python, maka jika kita menulis kode seperti ini:

def must_int(param: int) -> int:
      return param

Walaupun kita memberi informasi bahwa yang param adalah int dan response berupa int, kita tetap bisa melakukan seperti ini dan kode tetap berjalan:

name = "hallo"
res = must_int(name)

Setelah melihat di atas, mungkin muncul pertanyaan: lalu, untuk apa typehint?

Dari yang kemarin saya coba, typehint membantu dalam:

  • Kejelasan
  • Dukungan IDE

Dengan typehint, kita bisa melihat tanpa meraba-raba param yang perlu digunakan itu type datanya apa, tanpa harus melihat detail kode. Dan dukungan IDE untuk autocompletion pun jadi lebih enak.

Lalu bagaimana kalau kita sudah menggunakan typehint dan ingin melakukan pengecekan agar tidak sebatas tentang kejelasan kode dan dukungan IDE? Jawabannya adalah pustaka type checking. Di Python, setidaknya ada 3 yang populer: MyPy, Pyright, dan BasedPyright. Dari ketiga itu, yang sudah saya coba baru dua: MyPy dan BasedPyright.

Intinya, ketiga pustaka tadi membantu untuk pengecekan kode Python kita agar digunakan semestinya sesuai dengan typehint — seperti contoh di atas, ketika dilakukan pengecekan akan muncul error.

Typehint di Legacy Code

Kalau kita memulai aplikasi baru, terutama menggunakan kode dasar dari FastAPI, ini cukup "mudah", karena sejak awal FastAPI dengan Pydantic-nya cukup mengenalkan penggunaan type di kodenya. Tapi masalah muncul kalau kita mencoba memasang typehint dan typecheck di legacy code, dan kodenya adalah Django.

Salah satu alasan saya berhenti mencoba typehint sebelumnya adalah karena saya memaksa menggunakan MyPy dengan Django, sedangkan Django banyak magic class yang dari bawaannya cukup sulit untuk ditentukan type-nya. Bahkan untuk membuat generic pun malah menambah frustasi. Tapi, pemasangan secara berkala bisa saya lakukan dengan pustaka BasedPyright dan django-types.

django-types membantu membuat stub untuk internal Django, sehingga untuk penggunaan fungsi-fungsi internal Django bisa dibantu dengan stub ini.
BasedPyright konfigurasinya memungkinkan dipasang secara bertahap.

contoh kode yang gagal

def home_view(request: HttpRequest) -> HttpResponse:
    """Function-based view that renders a homepage with all Play objects."""
    plays: QuerySet[Play] = Play.objects.all()
    hello = "string"
    param = must_int(hello)
    # Pass the QuerySet to the template
    return render(request, "home.html", {"plays": plays, "params": param})


def must_int(param: int) -> int:
    return param

Saat saya menjalankan perintah basedpyright muncul peringatan

/Users/ariesm/code/python/wip/playg/views.py
  /Users/ariesm/code/python/wip/playg/views.py:12:22 - error: Argument of type "Literal['string']" cannot be assigned to parameter "param" of type "int" in function "must_int"
    "Literal['string']" is not assignable to "int" (reportArgumentType)
1 error, 0 warnings, 0 notes

Mari perbaiki menjadi:

def home_view(request: HttpRequest) -> HttpResponse:
    """Function-based view that renders a homepage with all Play objects."""
    plays: QuerySet[Play] = Play.objects.all()
    number = 123
    param = must_int(number)
    # Pass the QuerySet to the template
    return render(request, "home.html", {"plays": plays, "params": param})


def must_int(param: int) -> int:
    return param

Saat melakukan pengecekan menjadi:

~ basedpyright
0 errors, 0 warnings, 0 notes

Berikut adalah konfigurasi basedpyright yang saya gunakan, mayoritas konfigurasi ini menggunakan rekomendasi LLM ChatGpt dan disesuaikan dengan dokumentasi dan temuan di stackoverflow

[tool.basedpyright]
# gradual, only add app "playg"
include = ["playg"]
exclude = ["**/migrations", "**/__pycache__", "**/settings.py"]
# stub coming from django stub/types
extraPaths = ["typings"] # prepare for stub
pythonVersion = "3.13"
pythonPlatform = "Linux"
typeCheckingMode = "basic"
venvPath = "."

# Enable missing imports detection for better error tracking
reportMissingImports = true
reportMissingTypeStubs = false

# Reduce noise from dynamically typed Django parts
reportUnknownMemberType = false
reportUnknownVariableType = false
reportUnknownArgumentType = false
reportUntypedClassDecorator = false
reportUntypedFunctionDecorator = false
reportUntypedBaseClass = false
reportIncompatibleVariableOverride = true
reportAttributeAccessIssue = false  # Only if the stub doesn't fully fix it

[tool.django-types]
django_settings_module = "core.settings"

Untuk otomatisasi pengecekan kita bisa menggunakan pre-commit atau mungkin di pipeline CI yang dipakai.

Kesimpulan

Bagi saya, typehint di Django atau Python memang tidak bisa dipaksa sangat strict, apalagi kalau kita pakai framework seperti Django. Tapi dengan konfigurasi yang disesuaikan dengan kebutuhan, bisa sangat membantu kita dalam:

  • kejelasan kode
  • dukungan IDE yang optimal
  • menemukan potential bug

Read more