【Python #2】はじめてのPydantic
こんにちは! 最近FastAPIとRemixを趣味で触り始めた仙台オフィスのChloéです。
前回はmypyによる静的型チェックについての記事を書きましたが、今回はPydanticによってデータの検証を動的にを行う方法を紹介します。
目次
- 環境
- バリデーションの概説
- なぜバリデーションを行うのか
- 手続き的なバリデーションの問題点
- バリデーションライブラリによる解決
- Pydanticの基本
- Pydanticのインストール
- Pydanticの基本的な使い方
- 実践的な例
- 環境変数の読み込み
- カスタムバリデータ
- まとめ
1. 環境
- Python 3.12.6
- Pydantic 2.9.2
2. バリデーションの概説
2.1 なぜバリデーションを行うのか
アプリケーションにおけるデータの検証は、システムの信頼性と安全性を確保する上で不可欠ですが、この検証には大きく分けて2つのアプローチがあります。
1つは静的型チェックで、開発時にコードの型の整合性を検証し、明らかな型の不一致を事前に発見します。(興味のある方は前の記事を参照ください)
もう1つはランタイムバリデーションで、アプリケーションの実行時に外部から入力される具体的なデータを検証します。特にWebアプリケーションなどでは、APIリクエストやファイルアップロード、データベースからの読み込みなど、実行時まで内容が分からないデータを扱う必要があります。このようなデータに対して、ランタイムバリデーションによってデータが正しいかどうかを検出し、正しくないデータがアプリケーションの内部に入らないように制御できます。
静的型チェックとランタイムバリデーション、それぞれの特徴を活かしながら組み合わせることで、より安全で信頼性の高いアプリケーション開発が可能になります。
2.2 手続き的なバリデーションの問題点
前節で説明したとおり、ランタイムバリデーションは重要です。しかし、このバリデーションの実装を適切に管理しないと、開発の足かせとなってしまう可能性があります。
何もライブラリを導入しない状態でバリデーションロジックを実装しようとすると、大体以下のようなif文の連続になるのではないでしょうか。
# userリソースを作成する
def create_user(user_data: dict):
if "name" not in user_data:
raise ValueError("名前は必須です")
if not isinstance(user_data["name"], str):
raise ValueError("名前は文字列である必要があります")
if len(user_data["name"]) < 3:
raise ValueError("名前は3文字以上である必要があります")
if "age" in user_data:
if not isinstance(user_data["age"], int):
raise ValueError("年齢は整数である必要があります")
if user_data["age"] < 0:
raise ValueError("年齢は0以上である必要があります")
# さらにネストされたデータの検証...
このように、手続き的にバリデーションを行うアプローチには、以下のような問題があります。
- コードの可読性と保守性が低下する
- ネストされたデータ構造やオプショナルなフィールド等を扱おうとすると、条件分岐ベースのロジックは複雑になりがちです。
- 条件分岐が複雑になると、新しいバリデーションルールの変更や既存のルールの変更が困難になる可能性が高くなります。
- バリデーションロジックがアプリケーション全体に散らばってしまう
- バリデーションはアプリケーションにデータを入出力するたびに必要なので、素直な実装を行っているとバリデーションロジックがアプリケーション全体のあちこちに実装されてしまいます。その結果として、バリデーション自体のメンテナンス性や一貫性を損ねてしまうことになります。
- エラーメッセージを管理しにくい
- 一般的に、バリデーションに失敗した時にはエラーメッセージを出す必要がありますが、どのようなエラーが起きたときにどのメッセージを出すか管理するような実装が必要です。また、多言語対応やエラーの階層構造の表現を考えた場合、「エラーメッセージの管理」という機能だけでも実装すべき物が膨大になってしまいます。
2.3 バリデーションライブラリによる解決
前節で説明したような問題は、データの検証を行うバリデーションライブラリを使用することで簡潔に解決できます。
# Pydanticでの実装例
from pydantic import BaseModel, Field, NonNegativeInt
class User(BaseModel):
name: str = Field(
...,
# 3文字以上
min_length=3
# 日・英でエラーメッセージを出し分ける
description={
"ja": "ユーザーの名前(3文字以上)",
"en": "User name (minimum 3 characters)"
})
age: NonNegativeInt # 0以上の整数
# バリデーションが自動的に行われる
user = User.model_validate({"name": "John", "age", 25}) # OK
user = User.model_validate({"name": "Jo", "age", -1}) # バリデーションエラー (名前が3文字未満、年齢が負の値)
バリデーションライブラリを使うことにより、以下のようなメリットが得られます。
- コードの可読性と保守性
- バリデーション用のモデルクラスの定義により、各フィールドをどのように検証すべきかを宣言的に記述します。このため、フィールドの型や制約が一目で理解でき、新しいルールの追加や既存ルールの変更も簡単です。
- バリデーションロジックの集中管理
- モデルクラスとして1箇所でバリデーションルールを定義することで、アプリケーション全体で一貫したバリデーションを実現できます。同じロジックのバリデーションであれば、APIエンドポイント、フォーム処理、データベース操作などの各所に同じモデルを再利用できます。
- エラーメッセージの管理
- バリデーションルールとエラーメッセージを一緒に定義できるため、メッセージの管理が容易になります。多言語対応や、エラーメッセージのカスタマイズも、モデル定義の中で体系的に行うことができます。
3. Pydanticの基本
この章では、PythonのバリデーションライブラリとしてPydanticを紹介し、基本的な使い方を説明します。
PydanticはFastAPIやDjango NinjaといったWebアプリケーションフレームワークに組み込まれています。普段あまり意識せずに使っている方もいらっしゃるかもしれませんが、この章ではPydanticを単体で使うことを想定して説明します。
TypeScriptに慣れている方は、zod というバリデーションライブラリを使ったことがあるかもしれません。Pydanticはそれらと似たアプローチのライブラリなので、zod を知っている方ならば理解が容易だと思います。
3.1 Pydanticのインストール
Pydanticは、pip
などの各種パッケージマネージャからインストールできます。
pip install pydantic
poetryやuvを使っている場合は、ランタイム側の依存関係に含めてください。
uv add pydantic # --dev オプションを付けない
3.2 Pydanticの基本的な使い方
Pydanticを使って、バリデーション用のモデルクラスを定義して基本的なバリデーションを行ってみましょう。
- まず、必要なモジュールをimportします。
# pydantic用
from pydantic import BaseModel, Field, NonNegativeInt, ValidationError
# mypy用
from typing import Any, TYPE_CHECKING
- バリデーション用のモデルクラスを定義します。ここでは
User
クラスとします。User
クラスの各フィールドは以下の制約を持っています。name
: 文字列型・3文字以上age
: 整数型・0以上
class User(BaseModel):
name: str = Field(..., min_length=3)
age: NonNegativeInt
- バリデーションを実際に行う関数を定義します。関数には型注釈を付けておきます。
User
クラスのmodel_validate
にデータを渡すと、そのデータがUser
クラスの制約に合致するかをチェックした上でUser
クラスのオブジェクトを返してくれます。
def create_user(data: dict[str, Any]) -> User:
try:
return User.model_validate(data)
except ValidationError as e:
raise ValueError(f"Invalid user data: {e}")
- 実際にバリデーションを行ってみましょう。正常なデータを定義してバリデーションを行い、Userクラスのインスタンスが生成されることを確認します。
valid_data = {"name": "John", "age": 25}
user = create_user(valid_data)
print(f"user.name: {user.name}")
print(f"user.age: {user.age}")
# 実行すると以下のメッセージが表示されます:
# user.name: John
# user.age: 25
- 異常なデータを定義してバリデーションを行い、例外がraiseされることを確認します。
invalid_data = {"name": "Jo", "age": -1}
try:
user = create_user(invalid_data)
except ValueError as e:
print(f"Error: {e}")
# 実行すると以下のメッセージが表示されます:
# Error: Invalid user data: 2 validation errors for User
# name
# String should have at least 3 characters [type=string_too_short, input_value='Jo', input_type=str]
# For further information visit https://errors.pydantic.dev/2.9/v/string_too_short
# age
# Input should be greater than or equal to 0 [type=greater_than_equal, input_value=-1, input_type=int]
# For further information visit https://errors.pydantic.dev/2.9/v/greater_than_equal
- また、ここでmypyを使って「バリデーションを通過したオブジェクトには型注釈が付いている」ことを確認しましょう。まずは以下の実装を行います。(mypyの導入については前の記事を参照ください)
# create_user関数は既に定義済みとします
valid_data = {"name": "John", "age": 25}
user = create_user(valid_data)
if TYPE_CHECKING:
reveal_type(user.name)
reveal_type(user.age)
- 上記実装を行ったうえで、mypyを実行してください。以下の出力が表示されることにより、userオブジェクトが適切に型付けされていることが分かります。
% mypy myscript.py # あなたの実装したスクリプトファイルに名前を変えてください
myscript.py:19: note: Revealed type is "builtins.str"
myscript.py:20: note: Revealed type is "builtins.int"
Success: no issues found in 1 source file
- これで、Pydanticによって単純な構造のデータのバリデーションが行えました! 以下に完全なスクリプトを示します。
from pydantic import BaseModel, Field, NonNegativeInt, ValidationError
from typing import Any, TYPE_CHECKING
class User(BaseModel):
name: str = Field(..., min_length=3)
age: NonNegativeInt
def create_user(data: dict[str, Any]) -> User:
try:
return User.model_validate(data)
except ValidationError as e:
raise ValueError(f"Invalid user data: {e}")
valid_data = {"name": "John", "age": 25}
user = create_user(valid_data)
print(f"user.name: {user.name}")
print(f"user.age: {user.age}")
if TYPE_CHECKING:
reveal_type(user.name)
reveal_type(user.age)
invalid_data = {"name": "Jo", "age": -1}
try:
invalid_user = create_user(invalid_data)
except ValueError as e:
print(f"Error: {e}")
4. 実践的な例
この章では、実際のアプリケーションでありそうな実装例を2つ示します。
4.1 環境変数の読み込み
特にWebアプリケーションを実装していると、APIキーなどを環境変数から読み込むことが多くあるはずです。環境変数はただの文字列であるため、設定ミスに気が付きにくい問題がしばしば起こります。このようなケースをPydanticで対処してみましょう。
- まず、 Pydanticで環境変数を扱うため
pydantic-settings
というパッケージをインストールします。
pip install pydantic-settings
- スクリプトの実装を開始します。まず、必要なモジュールをimportします。
from pydantic import Field
from pydantic_settings import BaseSettings
from typing import Literal
from functools import lru_cache
BaseSettings
クラスを継承し、アプリケーションの設定クラスを実装します。- ここで、
env_file
の設定が重要です。pydantic-settings
は、システムに設定された環境変数を優先的に読み込み、システムに設定されていない環境変数をenvファイルから読み込もうとします。
- ここで、
class Settings(BaseSettings):
# アプリケーション全般の設定
env: Literal["development", "production"] = "development"
debug: bool = False
api_key: str = ""
# データベース設定
db_host: str = Field(default="localhost", alias="DB_HOST")
db_port: int = Field(default=5432, alias="DB_PORT")
db_username: str = Field(default="", alias="DB_USERNAME")
db_password: str = Field(default="", alias="DB_PASSWORD")
db_database: str = Field(default="", alias="DB_DATABASE")
db_ssl_mode: bool = Field(default=False, alias="DB_SSL_MODE")
model_config = {
"env_file": ".env"
}
Settings
クラスのオブジェクトを取得する関数を実装します。関数が呼ばれるたびにオブジェクトを生成し直す必要はないので、@lru_cache
デコレータでSettings
オブジェクトをキャッシュし、2回め以降の呼び出しでは最初に生成したSettings
オブジェクトを返すようにします。- ここで、デコレータという概念が登場します。デコレータは、この記事に出てくるサンプルコードにおいては「関数に対して外から機能を付加するもの」という理解で大丈夫です。(厳密な定義を把握しておきたい方は、公式ドキュメントや各種Pythonの参考書をご覧ください)
@lru_cache
def get_settings() -> Settings:
return Settings()
- 環境変数を読み込むテスト的なコードを実装します。
def show_settings():
settings = get_settings()
print("環境設定:")
print(f"ENV: {settings.env}")
print(f"DEBUG: {settings.debug}")
print(f"API_KEY: {settings.api_key}")
print("\nデータベース設定:")
print(f"HOST: {settings.db_host}")
print(f"PORT: {settings.db_port}")
print(f"USERNAME: {settings.db_username}")
# セキュリティのため、設定されているかどうかだけを表示
print(f"PASSWORD: {'*' * 8 if settings.db_password else 'Not set'}")
print(f"DATABASE: {settings.db_database}")
print(f"SSL_MODE: {settings.db_ssl_mode}")
if __name__ == "__main__":
show_settings()
- ここで、スクリプトファイルと同じディレクトリに
.env
という名前のファイルを作ります。内容は以下の通りとします。
# アプリケーション全般の設定
ENV=production
DEBUG=true
API_KEY=your-secret-api-key-12345
# データベース設定
DB_HOST=db.example.com
DB_PORT=5432
DB_USERNAME=dbuser
DB_PASSWORD=secure_password_123
DB_DATABASE=myapp
DB_SSL_MODE=true
- この状態で、スクリプトを実行します。以下のような出力が表示されれば成功です!
% python config_test.py # あなたの実装したスクリプトファイルの名前に変えてください (git)-[main]
ENV: production
DEBUG: True
API_KEY: your-secret-api-key-12345
データベース設定:
HOST: db.example.com
PORT: 5432
USERNAME: dbuser
PASSWORD: ********
DATABASE: myapp
SSL_MODE: True
- また、間違った設定をしてしまったときにエラーが出ることも試しましょう。
.env
ファイルのDB_SSL_MODE
を試しに以下の値に変更してみます。
DB_SSL_MODE=12345
- この状態で、スクリプトを実行します。以下のようなエラーが出れば想定通りです!
% python config_test.py # あなたの実装したスクリプトファイルの名前に変えてください (git)-[main]
Traceback (most recent call last):
-- 中略 --
pydantic_core._pydantic_core.ValidationError: 1 validation error for Settings
DB_SSL_MODE
Input should be a valid boolean, unable to interpret input [type=bool_parsing, input_value='12345', input_type=str]
For further information visit https://errors.pydantic.dev/2.9/v/bool_parsing
- Pydanticによって環境変数の読み込み時のエラーを検出できました! 完全なコードは以下の通りです。
from pydantic import Field
from pydantic_settings import BaseSettings
from typing import Literal
from functools import lru_cache
class Settings(BaseSettings):
# アプリケーション全般の設定
env: Literal["development", "production"] = "development"
debug: bool = False
api_key: str = ""
# データベース設定
db_host: str = Field(default="localhost", alias="DB_HOST")
db_port: int = Field(default=5432, alias="DB_PORT")
db_username: str = Field(default="", alias="DB_USERNAME")
db_password: str = Field(default="", alias="DB_PASSWORD")
db_database: str = Field(default="", alias="DB_DATABASE")
db_ssl_mode: bool = Field(default=False, alias="DB_SSL_MODE")
model_config = {
"env_file": ".env"
}
@lru_cache
def get_settings() -> Settings:
return Settings()
def show_settings():
settings = get_settings()
print("環境設定:")
print(f"ENV: {settings.env}")
print(f"DEBUG: {settings.debug}")
print(f"API_KEY: {settings.api_key}")
print("\nデータベース設定:")
print(f"HOST: {settings.db_host}")
print(f"PORT: {settings.db_port}")
print(f"USERNAME: {settings.db_username}")
print(f"PASSWORD: {'*' * 8 if settings.db_password else 'Not set'}")
print(f"DATABASE: {settings.db_database}")
print(f"SSL_MODE: {settings.db_ssl_mode}")
if __name__ == "__main__":
show_settings()
このように、Pydanticを使うと環境変数をより安全に扱うことができます。
4.2 カスタムバリデータ
Pydanticには組み込みのバリデータが実装されていますが、ときに自分でバリデータを実装したくなることがあります。そういった場合、Pydanticではモデル内にカスタムバリデータを実装できます。
本節では、仮にホテルの予約システムを考えます。このホテルには以下の仕様があるものとします。
- 毎週水曜日は休館日であり、その日に滞在することはできない
- 4連泊以上はできない
- チェックアウト日は必ずチェックイン日の後である必要がある (日帰りで利用することはできない)
- まず、スクリプトの先頭で必要なモジュールをimportします。
from datetime import date, timedelta
from pydantic import BaseModel, field_validator, ValidationInfo, ValidationError
BaseSettings
クラスを継承し、ホテルの予約クラスを実装します。
class RoomReservation(BaseModel):
check_in_date: date
check_out_date: date
- ホテルの予約クラスの中に、「特定の日付範囲が水曜日を含むか」を判定する関数を実装します。
# class RoomResavationの中に実装
@staticmethod
def has_closed_days(start_date: date, end_date: date) -> bool:
# 日付範囲に水曜日が含まれるかどうかを返す
# weekday == 2 は水曜日
return any(
(start_date + timedelta(days=i)).weekday() == 2
for i in range((end_date - start_date).days + 1)
)
- さきほど実装した
has_closed_days
関数を利用し、カスタムバリデータ用の関数を実装します。実装のポイントについて以下解説します。
- カスタムバリデータ用の関数には
@field_validator
と@classmethod
デコレータを付ける必要があります。(Pydantic v2からの仕様です) validate_dates
関数が呼ばれる時点では、Pydanticの組み込みバリデーション (check_in_date
及びcheck_out_date
が本当にdate
型かどうか) は既に済んでいることに留意してください。つまり、validate_dates
関数の中で改めて各フィールドのNone
チェック等を行う必要はありません。
# class RoomResavationの中に実装
@field_validator("check_out_date")
@classmethod
def validate_dates(cls, check_out: date, values: ValidationInfo) -> date:
check_in = values.data["check_in_date"]
# 宿泊期間のバリデーション
nights = (check_out - check_in).days
if nights < 1:
raise ValueError("チェックアウト日はチェックイン日より後である必要があります")
if nights >= 4:
raise ValueError("4泊以上の予約はできません")
# 休館日チェック
if cls.has_closed_days(check_in, check_out):
raise ValueError("予約期間に休館日(水曜日)が含まれています")
return check_out
- バリデーションのテストを行うコードを実装します。
def test_room_reservations() -> None:
# 正常系: 2泊3日の予約(水曜日を含まない)
reservation_data = {
"check_in_date": date(2024, 11, 10), # 日曜日
"check_out_date": date(2024, 11, 12) # 火曜日
}
try:
reservation = RoomReservation.model_validate(reservation_data)
print("テストケース1: 予約成功:", reservation)
except ValidationError as e:
print("テストケース1: バリデーションエラー:", e)
# 異常系: 4泊の予約を取ろうとしている
long_stay_data = {
"check_in_date": date(2024, 11, 8),
"check_out_date": date(2024, 11, 12)
}
try:
reservation = RoomReservation.model_validate(long_stay_data)
print("テストケース2: 予約成功:", reservation)
except ValidationError as e:
print("テストケース2: バリデーションエラー:", e)
# 異常系: 水曜日(休館日)を含む予約を取ろうとしている
wednesday_stay_data = {
"check_in_date": date(2024, 11, 12), # 火曜日
"check_out_date": date(2024, 11, 14) # 木曜日
}
try:
reservation = RoomReservation.model_validate(wednesday_stay_data)
print("テストケース3: 予約成功:", reservation)
except ValidationError as e:
print("テストケース3: バリデーションエラー:", e)
# 異常系: チェックアウト日がチェックイン日より前になってしまっている
invalid_data = {
"check_in_date": date(2024, 11, 12),
"check_out_date": date(2024, 11, 11)
}
try:
reservation = RoomReservation.model_validate(invalid_data)
print("テストケース4: 予約成功:", reservation)
except ValidationError as e:
print("テストケース4: バリデーションエラー:", e)
if __name__ == "__main__":
test_room_reservations()
- スクリプトを実行し、以下のような出力が表示されれば成功です!
% python custom_validator_test.py # あなたの実装したスクリプトファイルの名前に変えてください
テストケース1: 予約成功: check_in_date=datetime.date(2024, 11, 10) check_out_date=datetime.date(2024, 11, 12)
テストケース2: バリデーションエラー: 1 validation error for RoomReservation
check_out_date
Value error, 4泊以上の予約はできません [type=value_error, input_value=datetime.date(2024, 11, 12), input_type=date]
For further information visit https://errors.pydantic.dev/2.9/v/value_error
テストケース3: バリデーションエラー: 1 validation error for RoomReservation
check_out_date
Value error, 予約期間に休館日(水曜日)が含まれています [type=value_error, input_value=datetime.date(2024, 11, 14), input_type=date]
For further information visit https://errors.pydantic.dev/2.9/v/value_error
テストケース4: バリデーションエラー: 1 validation error for RoomReservation
check_out_date
Value error, チェックアウト日はチェックイン日より後である必要があります [type=value_error, input_value=datetime.date(2024, 11, 11), input_type=date]
For further information visit https://errors.pydantic.dev/2.9/v/value_error
- カスタムバリデータを実装し、フィールドのバリデーションロジックを制御できました! 完全なコードは以下の通りです。
from datetime import date, timedelta
from pydantic import BaseModel, field_validator, ValidationInfo, ValidationError
class RoomReservation(BaseModel):
check_in_date: date
check_out_date: date
@staticmethod
def has_closed_days(start_date: date, end_date: date) -> bool:
return any(
(start_date + timedelta(days=i)).weekday() == 2
for i in range((end_date - start_date).days + 1)
)
@field_validator("check_out_date")
@classmethod
def validate_dates(cls, check_out: date, values: ValidationInfo) -> date:
check_in = values.data["check_in_date"]
# 宿泊期間のバリデーション
nights = (check_out - check_in).days
if nights < 1:
raise ValueError("チェックアウト日はチェックイン日より後である必要があります")
if nights >= 4:
raise ValueError("4泊以上の予約はできません")
# 休館日チェック
if cls.has_closed_days(check_in, check_out):
raise ValueError("予約期間に休館日(水曜日)が含まれています")
return check_out
def test_room_reservations() -> None:
# 正常系: 2泊3日の予約(水曜日を含まない)
reservation_data = {
"check_in_date": date(2024, 11, 10), # 日曜日
"check_out_date": date(2024, 11, 12) # 火曜日
}
try:
reservation = RoomReservation.model_validate(reservation_data)
print("テストケース1: 予約成功:", reservation)
except ValidationError as e:
print("テストケース1: バリデーションエラー:", e)
# 異常系: 4泊の予約を取ろうとしている
long_stay_data = {
"check_in_date": date(2024, 11, 8),
"check_out_date": date(2024, 11, 12)
}
try:
reservation = RoomReservation.model_validate(long_stay_data)
print("テストケース2: 予約成功:", reservation)
except ValidationError as e:
print("テストケース2: バリデーションエラー:", e)
# 異常系: 水曜日(休館日)を含む予約を取ろうとしている
wednesday_stay_data = {
"check_in_date": date(2024, 11, 12), # 火曜日
"check_out_date": date(2024, 11, 14) # 木曜日
}
try:
reservation = RoomReservation.model_validate(wednesday_stay_data)
print("テストケース3: 予約成功:", reservation)
except ValidationError as e:
print("テストケース3: バリデーションエラー:", e)
# 異常系: チェックアウト日がチェックイン日より前になってしまっている
invalid_data = {
"check_in_date": date(2024, 11, 12),
"check_out_date": date(2024, 11, 11)
}
try:
reservation = RoomReservation.model_validate(invalid_data)
print("テストケース4: 予約成功:", reservation)
except ValidationError as e:
print("テストケース4: バリデーションエラー:", e)
if __name__ == "__main__":
test_room_reservations()
このように、複数のフィールドが関係する複雑なバリデーションを行う必要がある場合、カスタムバリデータが便利です。
5. まとめ
この記事では、Pydanticを用いたデータバリデーションについて説明しました。Pydanticを使用すると、Pythonの型ヒントを活用した堅牢なデータ検証が可能になり、特にAPIやアプリケーションの設定管理において大きな利点が得られます。
弊社で行っているようなプロトタイピングやMVPの開発など、高速な開発が求められる場面においても、データバリデーションは重要な役割を果たします。Pydanticを使用することで、型安全性が保証され、開発初期の段階から設定ミスや不正なデータの混入を防ぐことができます。これにより、後から発見される可能性のある問題を早期に特定でき、手戻りのリスクを最小限に抑えることができます。結果として、開発スピードを落とすことなく、品質の高いコードを書くことが可能になります。
どのようなバリデーションライブラリを使用するかはプロジェクトやチームの状況によりますが、PydanticはPythonの型ヒントと完全に統合された現代的なアプローチを提供し、特にFastAPIなどの最新のフレームワークとの相性も良好です。型安全性とパフォーマンスを両立させたい場合の有力な選択肢となるでしょう。
株式会社Biz Freakは、堅牢性と開発スピードを両立できるようなソフトウェア設計・実装に興味のあるPythonエンジニアを募集しています。
ご応募は下記のリンクよりどうぞ!お待ちしております。