コンテンツにスキップ

付録 — mypy / Pyrightから来た場合

静的型付けのベースラインがPythonのmypyやPyrightであれば、この付録でRigorとの語彙をマッピングする。ふたつのエコシステムは見かけ以上に多くを共有している — 漸進的型付けgradual typing、「ランタイムを壊さない」哲学、独立した型スタブファイル(.pyi/.rbs) — だが、アノテーションをどこに置くか、推論をどこまで積極的にするかで異なる選択をしている。

この付録の内容 5秒ピッチ · 型語彙マッピング · リファインメントキャリア · ナローイング · スタブ ↔ RBS · 深刻度とstrictモード · Pyright vs Rigor · 「アノテーション不要」 · ジェネリクス · Protocol ↔ RBSインターフェース · mypy/PyrightにあってRigorにないもの · Rigorにあってmypy/Pyrightにないもの · マイグレーションvignette

問いmypy / PyrightRigor
アノテーションはどこに書くか?ソース内(def f(x: int) -> str:.rbの隣の.rbsファイル
スタブ形式.pyiファイル.rbsファイル
アノテートされていないコードのデフォルトどこでもAny(mypy)/ 推論(Pyright)精密に推論、不明ならDynamic[Top]
strictモード--strict(mypy)/ strict: true(Pyright)severity_profile: strict
抑制# type: ignore[error-code]# rigor:disable <rule>
型の同一性名前的 + 構造的(Protocol)名前的 + 構造的ファセット
ナローイング(narrowing)フローセンシティブ(flow-sensitive)な型、型ガードフローセンシティブな型、述語メソッド + RBS::Extended

Python/Rubyの類似点は構文より深い: 両言語とも動的に生まれ、どちらも後から漸進的型付けを取り入れ、どちらも型チェックを助言的なものとして扱い、どちらも型ヒントの公式構文(Pythonのtyping、RubyのRBS)を提供する。Rigorの設計優先事項の多くはmypyが正しくやったことを反映している。

Pythonの型付けRigorの表現備考
intInteger
floatFloat
boolbool(`ConstantConstant`)
strString
bytesString(バイナリエンコーディングを持つ)Rubyには独立したbytes型がない。
NoneConstant<nil>nilはRubyの唯一の「値なし」。
AnyDynamic[Top]「ここは黙っていて」キャリア(carrier)。
objectObject(またはTopobjectはPythonの普遍的なスーパータイプ(Noneを含むすべて)。Rigorの最も近い対応はTop
Never / NoReturnBot空の型。
Optional[T] / `TNone`T?(すなわち`T
Union[A, B] / `AB``A
Literal[42]Constant<42>直接対応。
Literal["foo", "bar"]`Constant<“foo”>Constant<“bar”>`
Final[T](対応なし)Rigorはまだイミュータビリティを追跡しない。
tuple[int, str]Tuple[Integer, String]同じ位置ごとのモデル。
list[T]Array[T]
dict[K, V]Hash[K, V]
set[T]Set[T]
TypedDictHashShape{...}required/optionalキーを持つクローズドシェイプ(shape)。
NotRequired[T](TypedDict)HashShape内のオプショナルキーRigorのキーごとのrequired/optionalフラグでカバー。
Callable[[int], str]^(Integer) -> String(RBSのproc/block構文)
TypeVar('T')RBSの[T]型パラメータ
Generic[T]RBSのclass Foo[T]
Protocol(PEP 544)RBSのinterface _Foo構造的型付け(structural typing)。
runtime_checkable Protocol(対応なし)Rigorは構造的プロトコルに対してisinstanceを実行しない。
Self(PEP 673)RBSのself
ClassVar[T]シングルトン側のattr_* / self.@var
Annotated[T, "tag"]RBS::Extended%a{...}アノテーション両者とも型にメタデータを付与する。

リファインメントキャリアvs Pythonのアノテーション慣習

Section titled “リファインメントキャリアvs Pythonのアノテーション慣習”

Pythonの型システムはリファインメント(refinement、篩型とも)形状の機能を一度に一つずつ追加してきた(LiteralLiteralStringTypeIsAnnotated)。Rigorはより広いカタログを標準で提供する。

RigorのリファインメントPythonで最も近いもの
non-empty-string(ビルトインなし。PEP 675のLiteralStringは精神的に近いが意味論が異なる)
literal-stringLiteralString(PEP 675) — ソースコードのリテラルから構築されたことが証明可能。直接対応
positive-int(ビルトインなし。サードパーティのバリデーターでAnnotated[int, Gt(0)]という慣習)
int<min, max>(ビルトインなし。同じAnnotated[int, Range(...)]の慣習)
numeric-string(ビルトインなし)
non-empty-array[T](ビルトインなし。一部のライブラリはtuple[T, *tuple[T, ...]]を使う)
Constant<42>Literal[42]

LiteralStringが最も深い等価 — PythonのLiteralStringもRigorのliteral-stringも「この文字列はソースコードから来ており、ランタイム入力ではない」という事実を保持し、フォーマット/補間を通じて合成する。

ナローイング — 親しみやすい部分

Section titled “ナローイング — 親しみやすい部分”

両チェッカーともフローセンシティブ。ナローイングのプリミティブには直接対応するものがある:

PythonRigor
if x:if x — 真側のエッジからFalse/Noneを除去
if x is None:if x.nil?
if x is not None:unless x.nil?
isinstance(x, int)x.is_a?(Integer)
if isinstance(x, (int, str)):`if x.is_a?(Integer)
assert isinstance(x, T)プラグイン経由の# rigor:assert-typeスタイル、またはrigor-sorbet経由のT.cast
match x: case ...(PEP 634)case x; in ...(Rubyのパターンマッチング)
ユーザー定義TypeGuard[T](PEP 647)%a{rigor:v1:predicate-if-true: x is T}ディレクティブ
ユーザー定義TypeIs[T](PEP 742)同じディレクティブ — Rigorのナローイングはデフォルトで対称(truthy側とfalsey側の両方)
assert x is not None; x.upper()同じイディオム: unless x.nil?; x.upcase; end
cast(int, x)rigor-sorbet経由のT.cast(x, Integer)、またはRBS側のparam:ディレクティブ

注目: PythonのTypeGuardは一方向(truthy側だけをナローイング)だが、TypeIs(PEP 742、採択済み)は双方向。Rigorのpredicate-if-truepredicate-if-falseディレクティブは独立していて合成できる — デフォルトでpredicate-if-true: x is Tを宣言するとfalsey側もx is ~Tにナローイングされ、TypeIsに等価。

Pythonの.pyiファイルとRigorの.rbsファイルは同じ役割を果たす: インラインで型を提供しないライブラリの型を宣言する。

PythonRigor
.pyiスタブ.rbsファイル
typeshed(コミュニティメンテナーのスタブ)rbs_collection + Rigorの同梱stdlibカタログ
mypy_path設定.rigor.ymlsignature_paths:
py.typedマーカー(対応なし — Rigorはpaths:以下の任意のファイルを確認する)
from __future__ import annotations(対応なし — RBSはファイル分離の性質上常に遅延)
型を明らかにする: reveal_type(x)dump_type(x)(info診断)/ assert_type("...", x)

reveal_typedump_typeは名前が異なる同じツール — 両者ともcall-siteで推論された型を診断として発行し、どちらも慣用的なテストハーネスではランタイムでno-opで、どちらも「チェッカーはここで何を見ているか?」を調べる正規のプローブ。

深刻度、抑制、「strictモード」

Section titled “深刻度、抑制、「strictモード」”
Python(mypy)Rigor
--strictseverity_profile: strict
--strict-optionalRigorでは常にオン(独立したフラグなし)
--no-implicit-optionalRigorでは常にオン
--check-untyped-defsRigorでは常にオン
--disallow-untyped-defs(対応なし — Rigorはアノテーションを要求しない)
--disallow-any-explicit(対応なし)
# type: ignore# rigor:disable all
# type: ignore[error-code]# rigor:disable <rule>
# mypy: ignore-errors(ファイルスコープ)# rigor:disable-file all
mypy.ini / pyproject.toml.rigor.yml / .rigor.dist.yml

概念的なギャップ: mypyの--disallow-untyped-defsはアノテーションがどこにでも存在すべきというベースライン前提を反映している。Rigorはアノテーションを要求しない — 推論が常に最初の答えであり、RBSはエスケープハッチ。これにより採用がスムーズになる: 「このモジュール全体をアノテートするまでmypyは役に立たない」という段階がない。

Pyright(MicrosoftのTypeScript型チェッカー、Pylanceのエンジン)はmypyよりRigorに精神的に近い — 両者ともアノテーションの完全性より推論の深さと実用的なナローイングを優先する。

PyrightRigor
# pyright: ignore[reportError]# rigor:disable <rule>
pyright --stats(直接対応なし — rigor check --explainは漸進的フォールバックの決定を示す)
ボディからの推論された戻り型同様 — defボディが辿られ推論された戻りが伝播する
投機的推論(Pyrightは高速)Rigorの型オブジェクトはイミュータブルな共有構造。キャッシュ駆動の再計算はインクリメンタル
ファイルレベルでのstrict / basic / offの設定severity_profile:はプロジェクト全体。ファイルごとは# rigor:disable-file

Pyrightの「積極的に推論し、ナローイング」というオーサリングループを使ったことがあれば、Rigorは親しみやすい。最大の調整はRigorのアノテーションが.rbソースではなく.rbsファイルに存在することだ。

「アノテーション不要」— ここでも本当

Section titled “「アノテーション不要」— ここでも本当”

典型的なmypyのオンボーディング例:

def classify(n: int) -> Literal["zero", "positive", "negative"]:
if n == 0:
return "zero"
if n > 0:
return "positive"
return "negative"
result = classify(7)
# mypy: result: Literal['zero', 'positive', 'negative']

Rigorで対応するコード — アノテーションなし:

def classify(n)
return :zero if n.zero?
return :positive if n.positive?
:negative
end
result = classify(7)
assert_type("Constant<:zero> | Constant<:positive> | Constant<:negative>", result)

同じ精度。片方はパラメータとreturnのアノテーションを書くが、もう片方は書かない。

sigが必要なとき — パブリックライブラリ境界のため、パラメータバリデーションのため、def.return-type-mismatchを発火させたいとき — それは.rbソースではなくsig/<file>.rbsに書く。

両エコシステムともジェネリクスを持つ。Rigorのものはより保守的なRBSのもの。

PythonRigor(RBS経由)
T = TypeVar('T')メソッドまたはクラス名の後の[T]
def first(xs: list[T]) -> Tdef first: [T] (Array[T]) -> T
Generic[T]クラスclass Foo[T]
T = TypeVar('T', bound=Comparable)[T < Comparable](RBSのバウンド付き型パラメータ)
ParamSpec(現時点で対応なし)
TypeVarTuple(現時点で対応なし)
Concatenate[X, P](現時点で対応なし)

Rigorのジェネリクスカバレッジはより保守的なRBSのものと一致するが、一般的なケース(コレクション、ジェネリックコンテナへのメソッド、クラスレベル型パラメータ)はカバーしている。

PythonのPEP 544はProtocolによる構造的型付けを導入した。RubyのRBSは最初のリリース以来構造的なinterface _Fooを持っている。

class SupportsClose(Protocol):
def close(self) -> None: ...
interface _SupportsClose
def close: () -> void
end

closeを定義するクラス(正しいシグネチャで)は両方を満たす。どちらのシステムもクラスに継承を宣言することを要求しない — 構造的なマッチは暗黙的。

Rigorはsig/からRBSインターフェースを読む。RBS宣言されたパラメータが_SupportsCloseの場合、Rigorはmypy/PyrightがProtocolに対してチェックするのと同じように、call-siteの引数を構造的にチェックする。

Pythonから持ち越した注意点をひとつ: Rigorにおいて「protocol」はこれを意味しない。構造的型付け(structural typing)の概念はRBSのinterfaceであり、「protocol」は別の、プラグインが宣言する機能(パススコープの振る舞い契約)のために予約されている。プロトコルと構造的型付けの付録がこの区別を詳しく解説する。

mypy / PyrightにあってRigorにないもの

Section titled “mypy / PyrightにあってRigorにないもの”
  • TypeVarへの分散アノテーションTypeVar('T', covariant=True)。Rigorは標準ライブラリに基づくRBSの分散に依拠する。ユーザー側での分散オーサリングはない。
  • Final / イミュータビリティ追跡。Rigorは「この名前は再代入されない」をまだモデル化しない。
  • @overloadスタック。RBSはメソッドオーバーロードをサポートするが、Rigorのアナライザーのディスパッチロジックはmypyのパターンベースのオーバーロード解決より保守的。
  • デコレーター対応の型変換。Pythonの型エコシステムは関数の型を変換するデコレーターのサポートが発達している。Rubyの対応物は少なく、RigorはModule#prepend/define_method変換をまだモデル化しない。
  • async/await。RubyにはFiberとAsyncがあるが、非同期型のRBSサーフェス(surface)はPythonのCoroutine[T, U, V]より断片的。

Rigorにあってmypy / Pyrightにないもの

Section titled “Rigorにあってmypy / Pyrightにないもの”
  • メソッド呼び出しを通じた定数folding。mypyもPyrightもリテラルをfoldするが、どちらも任意のビルトインメソッドを通じたfoldはしない。RigorはNumericStringSymbolArrayHash上のカタログ化された純粋メソッドのセットを通じてfoldする。
  • 自動ナローイングを持つファーストクラスのリファインメントキャリアnon-empty-stringpositive-intnumeric-stringint<min, max> — 述語で制限された値が対応するRubyの述語メソッドでナローイングされる。
  • false-positiveなしのスタンス。mypyは--no-warn-unused-ignores--ignore-missing-importsを設定しない限り動的コードについて警告する。Rigorは設定なしでDynamic[Top]には沈黙する。
  • 引数シェイプによる戻り型変化のプラグインサイド。Pyrightの「型エイリアスナローイング」とmypyのオーバーロードスタックがいくつかのケースをカバーする。Rigorのプラグイン契約(contract)はディスパッチポイントで完全なRubyコードを提供する。rigor-lisp-evalの例が標準デモ — Lisp.eval([:+, 1, 2])Integerを返し、Lisp.eval([:<, 1, 2])boolを返す。

mypy-tightenedなPythonモジュールをRubyに移植している。元のコード:

def classify_input(s: str) -> Literal["empty", "numeric", "text"]:
if not s:
return "empty"
if s.isdigit():
return "numeric"
return "text"
def shout(s: str) -> str:
assert s, "expected non-empty"
return s.upper()

Rigorの移植:

lib/text_utils.rb
def classify_input(s)
return :empty if s.empty?
return :numeric if s.match?(/\A\d+\z/)
:text
end
def shout(s)
raise ArgumentError if s.empty?
s.upcase
end
sig/text_utils.rbs
%a{rigor:v1:return: Constant<:empty> | Constant<:numeric> | Constant<:text>}
def classify_input: (String s) -> Symbol
%a{rigor:v1:param: s is non-empty-string}
def shout: (String s) -> non-empty-string

得られるもの: s.empty?は認識されたリファインメントナローワー(assert sは不要)。match?(/\A\d+\z/)はまだnumeric-stringにナローイングしない(v0.1.1のロードマップにある — docs/ROADMAP.md参照)が、最終的な動作はPyrightでのs.isdigit()ナローイングを反映する。

この付録セクションの残りを順番に読む必要はおそらくない。3つの有用なポインタ:

他のツールと比較したい場合は、兄弟付録ページがTypeScriptPHPStanSteepTypeProfをカバーしている。

© 2026 TypedDuck. Licensed under CC BY-SA 4.0.