S-JIS[2026-02-21/2026-02-22]

Rust PyO3サブモジュールメモ

Rustpyo3のPythonサブモジュールのメモ。


概要

Pythonのサブモジュールは、PyO3でもRustのモジュールで実装できる。


サブモジュールの例

pyo3 0.27でサブモジュールを実装する方法は2通りある。


インラインモジュール方式

親モジュールの中にネストしたサブモジュールを実装する方法。

submodule-example/src/lib.rs:

use pyo3::prelude::*;

/// 親モジュール
///
/// NOTE:
///     サブモジュールの実験
#[pymodule]
mod submodule_example {
    use pyo3::prelude::*;
    /// インライン サブモジュール
    #[pymodule]
    mod submodule {
        use pyo3::prelude::*;

        #[pyfunction]
        fn func() -> String {
            "submodule.func".to_string()
        }
    }
}

呼び出すPython側の例

$ uv run python
>>> import submodule_example
>>> submodule_example.submodule.func()
'submodule.func'

モジュールエクスポート方式

外側に定義したサブモジュールをエクスポートする方式。

submodule-example/src/lib.rs:

use pyo3::prelude::*;

/// 親モジュール
///
/// NOTE:
///     サブモジュールの実験
#[pymodule]
mod submodule_example {
    use pyo3::prelude::*;

    #[pymodule_export]
    use super::child_module;
}

/// サブモジュール
#[pymodule]
mod child_module {
    use pyo3::prelude::*;

    #[pyfunction]
    fn func() -> String {
        "child_module.func".to_string()
    }
}

呼び出すPython側の例

$ uv run python
>>> import submodule_example
>>> submodule_example.child_module.func()
'child_module.func'

別ソースファイルにする例

サブモジュールを別のソースファイルで定義する例。

submodule-example/src/sub.rs

use pyo3::prelude::*;

pub(crate) use sub::*;
/// 別ファイルのサブモジュール
#[pymodule]
pub(crate) mod sub {
    use pyo3::prelude::*;

    /// 別ファイルのサブモジュールのクラス1
    #[pyclass]
    pub struct SubClass1 {}

    /// 別ファイルのサブモジュールのクラス2
    #[pyclass]
    pub struct SubClass2 {}
}

Pythonのサブモジュールとして扱うためには、modに#[pymodule]を付ける必要がある。
なので、わざわざmodを定義している。

ただ、sub.rs内にsubというモジュールを作ると、Rustとしてはcrate::sub::subになってしまう。
これだと他のソースファイルから使うには不格好なので、「use sub::*;」によってcrate::subとして扱えるようにしてみた。

submodule-example/src/lib.rs:

use pyo3::prelude::*;

mod sub;
/// 親モジュール
///
/// NOTE:
///     サブモジュールの実験
#[pymodule]
mod submodule_example {
    use pyo3::prelude::*;

    #[pymodule_export]
    use super::sub::sub;

    #[pyfunction]
    pub fn create_sub_class1() -> super::sub::SubClass1 {
        super::sub::SubClass1 {}
    }
}

呼び出すPython側の例

$ uv run python
>>> import submodule_example
>>> help(submodule_example.sub.SubClass1)
Help on class SubClass1:

class SubClass1(builtins.object)
 |  別ファイルのサブモジュールのクラス1

>>> submodule_example.create_sub_class1()
<sub.SubClass1 object at 0x00000270039D8870>

例外クラスの例

例外クラスをerror.rsで実装する例。

サブモジュールを別ファイルにする例と同様にpymoduleの中で例外クラスを定義してみたが、create_exception!マクロによって作られた例外クラスはエクスポートされなかった。
また、create_exception!マクロには#[pymodule_export]を付けることが出来ない。(コンパイルエラーになる)

そこで、modの外側でcreate_exception!マクロによって例外クラスを定義し、pyclassの中でエクスポートする。

submodule-example/src/error.rs

use pyo3::{exceptions::PyException, *};

/// 例外モジュール
#[pymodule]
pub(crate) mod error {
    #[pymodule_export]
    use super::{MyError1, MyError2, MyError3};
}

create_exception!(submodule_example.error, MyError1, PyException, "エラー1");
create_exception!(submodule_example.error, MyError2, PyException, "エラー2");
create_exception!(submodule_example.error, MyError3, MyError1, "エラー3");

submodule-example/src/lib.rs:

use pyo3::prelude::*;

mod error;
/// 親モジュール
///
/// NOTE:
///     サブモジュールの実験
#[pymodule]
mod submodule_example {
    use pyo3::prelude::*;

    #[pymodule_export]
    use super::error::error;

    #[pyfunction]
    pub fn raise1() -> PyResult<()> {
        use crate::error::MyError1;

        Err(MyError1::new_err("test1"))
    }
}

呼び出すPython側の例

$ uv run python
>>> import submodule_example
>>> submodule_example.raise1()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
submodule_example.error.MyError1: test1

>>> try:
...     submodule_example.raise1()
... except submodule_example.error.MyError1 as e:
...     print(f"1: {e}")
... 
1: test1

pyo3-stub-genの例

pyo3-stub-genによってサブモジュール有りの型スタブファイルを作るには、プロジェクトの構成をPython/Rust混合構成にする必要がある。

また、型スタブを出力する対象のクラスや関数に、サブモジュール名を付ける必要がある。


PyO3のデフォルトのプロジェクト構成は「Pure Rust layout」という構成である。
pyo3-stub-genによって型スタブファイルを生成すると、プロジェクトディレクトリー直下に1ファイルだけ作られる)

この状態でサブモジュールを実装してstub_genを実行すると、以下のようなエラーになる。

error: Pure Rust layout does not support multiple modules or submodules. Found 4 modules: 'submodule_example', 'submodule_example.child_module', 'submodule_example.sub', 'submodule_example.submodule'. Please use mixed Python/Rust layout (add `python-source` to [tool.maturin] in pyproject.toml) if you need multiple modules or submodules.

「mixed Python/Rust layout(Python/Rust混合構成)」にする必要がある。
(Python/Rust混合構成にすると、stub_gen実行時にサブモジュール毎ディレクトリーが作られ、それぞれのディレクトリーの下に型スタブファイルが出力される)


Python/Rust混合構成への変更方法

Python/Rust混合構成(mixed Python/Rust layout)は、Rustのソースコードの他にPythonのソースコードも扱う構成。

Pythonプログラムから呼ばれるコードはPythonソースコードに記述し、そこからRustモジュールを呼び出す形になる。
型スタブファイル(拡張子pyiのファイル)もPythonソースコードと同じサブディレクトリーに配置されることになる。


Python/Rust混合構成にするには、pyproject.tomlに[tool.maturin]の設定を追加する。

ついでに、cache-keysにpyファイルも含めておくと(pyファイルを更新した際もuv run実行時に再ビルドされるので)便利。

submodule-example/pyproject.toml:

〜
[tool.uv]
cache-keys = [{ file = "pyproject.toml" }, { file = "src/**/*.rs" }, { file = "Cargo.toml" }, { file = "Cargo.lock" }, { file = "python/**/*.py" }]

[tool.maturin]
python-source = "python"

それから、pythonというディレクトリーを作り、その下にPythonモジュール名のディレクトリーを作る。
さらに、そのディレクトリーの下に__init__pyを作成する。

submodule-example/python/submodule_example/__init__.py:

from .submodule_example import *

__all__ = [name for name in globals() if not name.startswith("_")]

すなわち、以下のようなディレクトリー構成になる。

※stub_genを実行すると、python/モジュール/の下にサブモジュールのディレクトリーが作られ、それぞれの下に__init__.pyiファイルが生成される。


サブモジュール名の指定

pyo3-stub-genで出力対象とするクラスや関数には#[gen_stub_pyclass]#[gen_stub_pyfunction]を付けるが、さらにモジュール名も付ける必要がある。

※サブモジュール以外でもモジュール名を付けないと、出力対象外になってしまう。


クラスの場合はpyclassのmoduleでモジュール名を付ける。

#[pymodule]
pub(crate) mod sub {
    use pyo3::prelude::*;
    use pyo3_stub_gen::derive::*;
    /// 別ファイルのサブモジュールのクラス1
    #[gen_stub_pyclass]
    #[pyclass(module = "submodule_example.sub")]
    pub struct SubClass1 {}

    #[gen_stub_pymethods]
    #[pymethods]
    impl SubClass1 {
         〜
    }
}

関数の場合はgen_stub_pyfunctionのmoduleでモジュール名を付ける。

    /// インライン サブモジュール
    #[pymodule]
    mod submodule {
        use pyo3::prelude::*;
        use pyo3_stub_gen::derive::*;

        #[gen_stub_pyfunction(module = "submodule_example.submodule")]
        #[pyfunction]
        fn func() -> String {
            "submodule.func".to_string()
        }
    }

例外クラスの場合は、(通常通り)create_exception!マクロにモジュール名を指定する。[2026-02-22]

ただし、pyo3のcreate_exception!マクロではなく、pyo3-stub-genのcreate_exception!マクロ に変更する。

submodule-example/src/error.rs:

use pyo3::{exceptions::PyException, *};
use pyo3_stub_gen::create_exception;

/// 例外モジュール
#[pymodule]
pub(crate) mod error {
    #[pymodule_export]
    use super::{MyError1, MyError2, MyError3};
}

create_exception!(submodule_example.error, MyError1, PyException, "エラー1");
create_exception!(submodule_example.error, MyError2, PyException, "エラー2");
create_exception!(submodule_example.error, MyError3, MyError1, "エラー3");

型スタブファイルの生成

pyo3-stub-genのstub_genを実行すると、pythonディレクトリーの下にサブモジュールのディレクトリーが作られ、その下に型スタブファイル(__init__.pyi)が生成される。

> cd submodule-example
> cargo run --bin stub_gen

pdoc3の例

pdoc3でAPIドキュメントを生成する場合も、Python/Rust混合構成にする必要がある。

それから、pythonディレクトリーの下のサブモジュールのディレクトリーを手動で作り、それぞれのディレクトリーの下に__init.pyを置く。

トップレベルディレクトリー以外の__init__.pyの内容は以下のようにする。

submodule-example/python/submodule_example/child_module/__init__.py:

from submodule_example.submodule_example import child_module as _rust

for _name in _rust.__all__:
    globals()[_name] = getattr(_rust, _name)

__all__ = _rust.__all__

submodule_example.submodule_exampleは、Rust側で定義されたsubmodule_exampleモジュールを指しているらしい。(ビルドして作られたpydファイル内にあるらしい)



submodule_exampleパッケージのDocstringを書きたい場合は、(Rustのソースコード上ではなく)submodule_example/__init__.pyに書く。

submodule-example/python/submodule_example/__init__.py:

"""
サブモジュールの実験(トップ)
"""

from .submodule_example import *

__all__ = [name for name in globals() if not name.startswith("_")]

HTML生成

これで、pdoc3でサブモジュール付きのHTMLが生成できる。

> submodule-example
> uv run pdoc submodule_example -o docs --html --force

> tree /f docs
C:\TMP\SUBMODULE-EXAMPLE\DOCS
└─submodule_example
    │  index.html
    │  submodule_example.html
    │
    ├─child_module
    │      index.html
    │
    ├─error
    │      index.html
    │
    ├─sub
    │      index.html
    │
    └─submodule
            index.html

ただ、生成されたHTMLを見ると、サブモジュール一覧に「submodule_example.submodule_example」が出ているのが気になる。

これを消すには、HTML生成時にフィルター条件を付けるスクリプトを用意する。

submodule-example/generate_docs.py:

import pdoc
import os

def docfilter(obj):
    return "submodule_example.submodule_example" not in obj.refname

for mod_name in ["submodule_example", "submodule_example.submodule", "submodule_example.sub", "submodule_example.child_module", "submodule_example.error"]:
    out_path = f"docs/{mod_name.replace('.', '/')}/index.html"
    os.makedirs(os.path.dirname(out_path), exist_ok=True)
    with open(out_path, "w", encoding="utf-8") as f:
        f.write(pdoc.html(mod_name, docfilter=docfilter))

スクリプトの実行例

> cd submodule-example
> uv run python generate_docs.py

だたまぁ、こんなスクリプトを使う方法よりも、Rustのモジュールを不可視にする(モジュール名の先頭にアンダースコアを付ける)方が素直だと思う。


pyo3へ戻る / Rustへ戻る / 技術メモへ戻る
メールの送信先:ひしだま