Let's Django!Let's Django!

這是 Django Tutorial 的第 10 篇。

範例程式碼可參考我的 GitHub 專案

本文相關的程式碼改動,都集中在這個 PR


在〈《強健的 Python》筆記(一)Type Hints 的成本與挑戰〉一文中,我們探討了 type hints(type annotation)對 Python 專案開發的重要性,並在〈Python type checker:Mypy 介紹〉中介紹了目前最常用的 Python 型別檢查器——Mypy。

我想,是時候為你的 Python 專案加入型別檢查了。

本文介紹如何在 Django 專案中整合 Mypy,並提供一些實際例子來幫助你快速上手。透過這些步驟,你將能夠更好地利用型別檢查來提高程式碼的穩定性和可維護性。

系列:Mypy 三部曲

  1. 《強健的 Python》筆記(一)Type Hints 的成本與挑戰
  2. Python type checker:Mypy 介紹
  3. Django 專案加入 Mypy 指南

有多少 Django 專案使用 Type Hints?

你可能覺得 type hints 還很遙遠,甚至是比單元測試更加稀有的存在。畢竟在 Python 中,型別是完全「可選」的——就像測試一樣😆

然而,隨著社群的成長和工具的進步,越來越多 Django 開發者開始採用 type hints。

那具體是多少呢?

依照 JetBrains 對 Django 開發者的這份調查,竟然已有一半了!(出乎我的意料)

Django Developers Survey 2023Django Developers Survey 2023

所以,為 Django 專案加入 type hints,已然是大勢所趨,今天就一起來實踐吧!

要為 Python 程式碼加上 type hints,關鍵在於 static type checker,也就是靜態型別檢查器。而 Mypy 正是其中最受歡迎的選擇。

Django 專案整合 Mypy,大概有以下幾步:

  1. 建立並修改 Mypy 設定檔。
  2. 安裝 django-stubs。
  3. 安裝 VS Code Mypy 套件。
  4. pre-commit 整合(與其中的問題)。

讓我們一一解說。


一、建立 Mypy 設定檔

Mypy 支援好幾種常見的設定檔。

最常見的是mypy.ini,它同時也支援pyproject.toml,而且很多大型開源專案都會採用後者,比如 FastAPI

上述兩種最推薦,這裡我們先採用mypy.ini

不建立設定檔,也能使用 Mypy。設定檔主要是讓你的型別檢查更加客製化。尤其用來排除那些你不需要的規則。

Django 特殊設定

但對於 Django 專案來說,設定檔則是「必要」,因為有幾處必須定義——否則 Mypy 會跳出一大堆錯誤XD。

這是因為 Django 有很多動態屬性(執行時才會確定內容),少了這些設定,Mypy 很可能無法正確檢查。

我們看一下設定內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[mypy]
# 排除所有 migrations 資料夾和 manage.py
exclude = ^(migrations|.*manage\.py)$

warn_return_any = true
disallow_untyped_calls = true
allow_redefinition = true
check_untyped_defs = true
ignore_missing_imports = true
incremental = true
strict_optional = true
show_traceback = true
warn_unused_ignores = true
warn_redundant_casts = true
warn_unused_configs = true
warn_unreachable = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
disable_error_code = empty-body
force_uppercase_builtins = true
force_union_syntax = true

plugins =
mypy_django_plugin.main

[mypy.plugins.django-stubs]
django_settings_module = DjangoTutorial.settings

對 Django 專案來說,其中重點有三:

1
2
3
4
5
6
7
exclude = ^(migrations|.*manage\.py)$
...
plugins =
mypy_django_plugin.main

[mypy.plugins.django-stubs]
django_settings_module = <你的 Django 專案名稱>.settings

exclude中把 db 遷移檔資料夾和 Django 自帶的manage.py排除檢查,這是很常見的設定,強烈建議加上。

plugins一定要有mypy_django_plugin.main(它的值可以是多個)。

django_settings_module設定也是必要的,要使用你自己的 Django 專案名稱。

其實,上述設定大多是我從 django-stubs 的 mypy.ini 照搬過來的。

而且它在註解有聲明,其餘專案也可以適用:

Regular configuration file (can be used as base in other projects, runs in CI)

大部分設定我都直接套用了,以免 Mypy 的檢查太過寬鬆。如果想進行調整,則需要了解 Mypy 的設定細節,這部分請參考官方文件


二、安裝 django-stubs

Type hints 是在 Python 3.5 才引入的功能,而在此之前誕生的套件和框架,自然不會有 type hints。

少部分的套件可能會在後期加入 type hints,比如 Flask,在 2.0 版本加入了極大量的 type hints,但大多數套件仍需要額外的 stubs 檔來補充。

什麼是 Stubs?

所謂的 stubs(不是測試的那種),是指用來補充那些沒有 type hints 的模組或框架的檔案。它們通常是 .pyi 格式,類似於介面定義檔案,提供了函式、類別、方法等的型別資訊——但不會包含實際邏輯。

舉例而言,這是 Django ORM QuerySet 方法中的bulk_create的 stubs:

1
2
3
4
5
6
7
8
9
def bulk_create(
self,
objs: Iterable[_T],
batch_size: int | None = ...,
ignore_conflicts: bool = ...,
update_conflicts: bool | None = ...,
update_fields: Sequence[str] | None = ...,
unique_fields: Sequence[str] | None = ...,
) -> list[_T]: ...

django-stubs 就是為 Django 提供的 type hints 補充包,使用 django-stubs 可以讓 Django 在不修改原始程式碼的情況下,享受到 type hints 的優勢。


三、VS Code Mypy 套件整合

如果你使用 VS Code IDE,那 Mypy 套件肯定是必須的,它能讓你在開發的當下就得知 Mypy 的型別錯誤提示。

該套件也會自動讀取專案根目錄下的 Mypy 設定檔,如果有設定檔,建議就不要在 VS Code 的 settings.json再額外設定 Mypy 套件的行為,以免衝突。

和其它開發類套件(如 linter、formatter)相同,該套件已自帶了一個 Mypy 版本,但如果你的虛擬環境中有安裝 Mypy,則會優先使用虛擬環境中的版本。

VS Code 的 Mypy 套件整合很簡單,卻非常實用。強烈建議你安裝。


四、pre-commit(與相關問題)

Mypy 有 pre-commit 的 hook,但存在一些整合問題。

我遇到的問題是,Mypy hook 雖然可以正常執行,但無法正確讀取專案的 Mypy 設定檔。也就是說,它只會照「預設模式」來執行檢查,這就不太實用了。

即使在 pre-commit 的 hook 設定檔中加入下列參數也沒用:

1
2
3
hooks:
- id: mypy
args: [--config-file=mypy.ini]

我後來還是找不到原因。我們知道,pre-commit 每一個 hook 都擁有自己獨立的執行虛擬環境——顯然這個環境還不健全!

2024/09/09更新:Mypy 的 pre-commit 設定確實相對複雜,網友 geminixiang 提供了一個不錯的做法,不需要使用 local hook,而我也依此修改了一個自己的版本。詳情請見下方留言區。

本地 Hook

一個釜底抽薪之計,就是建立 Mypy 的 本地 pre-commit hook。

不過做法上較為複雜,而且可能在不同開發者之間產生環境差異,我感覺也不是特別好的辦法。

有興趣的人可以參考這篇〈Running Mypy in Pre-commit〉,裡面有詳細的步驟。

基於上述理由,本次範例專案的程式碼改動,沒有加入 Mypy pre-commit 設定。

我想最好的辦法,還是在 CI 階段執行 Mypy。這個我們另篇文章再談吧!


結語:Type Hints 與未來

Mypy 與 Django 的整合,雖然需要花一些心思,但一旦完成,對於程式碼的穩定性與可維護性都會帶來顯著提升。

希望透過這篇文章,你已經掌握了如何為 Django 專案加入 Mypy 的基本步驟,並了解了在整合過程中可能遇到的挑戰。

期許未來所有 Python 專案使用 type hints 的比例,會愈來愈高,這樣我們就能更好地利用 Python 的動態特性,同時又享受到靜態型別語言的優勢。