🤧

個人開発OSSが世界に勝てなかった話

2024/05/24に公開

ゆーすけべー氏の「OSSで世界と戦うために」にインスパイアされました。5年間pyserdeというOSSのライブラリを開発・メンテしてきた筆者が、ちょっとだけ世界と戦ってみたけど全然勝てなかったという話です。Honoとはプロダクトの規模も開発にかける情熱も全然違うけど、単純にポストモーテムは読み物として面白いかなと思ったので書いてみます。また、5年間の開発で学んだやってよかったことや失敗などもシェアできればと思います。

pyserde

pyserdeは筆者が2019年から開発メンテナンスしているOSSで、RustのserdeというライブラリにインスパイアされたPython用のシリアライゼーションフレームワークです。

https://github.com/yukinarit/pyserde

以下のようにクラスを定義すると、型アノテーションに基づいたデータ変換やバリデーションのコードが内部的に生成され、強い型付けのクラスを生成することができます。強い型付けのクラスはランタイムに型を自動的に検査しエラーを投げてくれます。mypyやpyrightのようなタイプチェッカーと組み合わせると、静的・動的両方で型チェックでき、Rustのような静的言語と近い水準まで厳密な型でプログラミングできるようになります。

@serde                           <- serde デコレータをつける
class Location:
  id: str                        <- PEP484で型アノテーションをつける
  parking_type: PowerType        <- enum, list等様々な型が使用可能
  evses: list[EVSE]              <- 他のserdeクラスもネスト可能
  country: Annotated[str, MaxLen(3)]   <- typing.Annotatedで付加的なバリデーション追加

例えば以下のJSONからLocationへデシリアライズすると、

print(from_json(Location, '{"id":"1",
                            "power_type":"AC_1_PHASE",
                            "country":"JAPAN",
                            "evses": [{"uid":"10","status":"AVAILABLE"}]}'))

モデルのスキーマ定義でcountryMaxLen(3)と定義しているため、以下のエラーが出ます。

serde.compat.SerdeError: Method __main__.Location.__init__() parameter country='JAPAN' violates type hint typing.Annotated[str, Is[lambda text: text is None or len(text) <= max_length]], as str 'JAPAN' violates validator Is[lambda text: text is None or len(text) <= max_length]:
    False == Is[lambda text: text is None or len(text) <= max_length].

このように従来の動的言語でのプログラミングとは異なり、より型安全なプログラミングが可能となります。

できること

  • Data Format: dict, tuple, JSON, YAML, TOML, MsgPack, Pickle
  • Supported types
    • dict, list, set, frozenset, defaultdict, Union, Optional, Any
    • NewType, Literal, Generic, ClassVar, InitVar, Enum
    • pathlib, decimal, uuid, datetime, ipaddress, numpy, SQLAlchemy (comming soon) …
  • Case conversion, Rename, Alias, Skip, Flatten
  • Custom field/class/global (de)serializer
  • Strict type check, type coercing

詳しくはpyserde Guideを参照。

つくった動機

筆者は2018年頃、某インターネット証券会社でマーケットデータやオプショントレーディングのバックエンドを開発していました。増え続けるサービスの開発生産性、品質を上げるために、主力言語であったC++、Rust、Pythonで使える社内RPCフレームワーク、コードジェネレータを開発することにしました。もちろんgRPCはその時あったし、注目もしていたんですが、当時はRustのサポートが不十分であったことや、特殊な通信要件(IP Multicast等)があったため採用しませんでした。

社内フレームワークの主な仕様

  • プログラミング言語: C++17, Rust, Python
  • シリアライゼーション: MsgPack
  • トランスポート: ZeroMQ (TCP, Uni/Multicast, IPC etc.)
  • サポートする機能: ネットワーキング、Web、データ永続化

同時期にRustでバックエンド開発を始めるのですが、Rustの表現力、安全性、生産性に驚かされ、特にserdeのようなProcedual Macroベースのライブラリに感銘を受けます。Pythonで同じことできないかなと考えてPython 3.7で追加されたdataclassesのコードリーディングをしていたところ、モジュールがロードされた時に__init__や__repr__、__eq__のコード生成することによって手書き実装とほぼ変わらない性能を実現していることが分かりました。あぁこれ面白いなと思って、同じ仕組みでserdeのようなライブラリ作れないかなと思ったのがきっかけです。

pydantic

Pythonを使っていてpydanticを知らない開発者は少ないと思います。pydanticは型アノテーションを利用したデータバリデーションライブラリで、FastAPIにも採用されていることで有名です。

https://github.com/pydantic/pydantic

以下のようにpydantic.BaseModelを継承することで、型アノテーションに基づいたデータ変換やバリデーションのコードが内部的に生成され、強い型付けのクラスを生成することができます。強い型付けのクラスはランタイムに型を自動的に検査しエラーを投げたり、自動的に値を変換してくれたりします。

from datetime import datetime
from pydantic import BaseModel

class User(BaseModel):
  id: int
  name: str = 'John Doe'
  signup_ts: datetime | None = None
  friends: list[int] = []

user = User(id='123', signup_ts='2017-06-01 12:22', friends=[1, '2', b'3'])

print(user)
#> User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]

そもそもなんで勝負しようと思ったの?

ありがたいことにXやGithub上でpyserdeとpydanticについて言及してくれていることに気が付きました。

https://x.com/StartedTwittter/status/1317805854570160128
https://x.com/toda_wase/status/1704822237705212162
https://github.com/yukinarit/pyserde/discussions/409

pyserdeを開発し始めた当初はpydanticのことは知らなかったし、開発中も意識することは無かったんですが、pydanticじゃなくてあえてpyserdeを使ってくれているという声を聞いたりして、

「あれ、もしかしてpydanticに勝てるかも?🤔」って思い始めたりしました。
















結果

全然勝てませんでした\(^o^)/






勝てなかった理由ってなんだろう?🤔








勝てなかった理由

理由1: 1人対企業

pyserdeのフルタイムの開発者は一人もおらず、筆者一人が空き時間に開発しています。一方pydanticは法人になっており2024年5月現在で10人のフルタイムの開発者がいます。開発力の差は目に見えていて、pydanticはライブラリ・バックエンドをRustで書き直したり、mypyのプラグインがあったり、numpy, SQLAlchemyを始めとした様々なパッケージとのインテグレーションがあるだけでなく、カッコいいランディングページやpydanticから派生したプロダクトもたくさんあります。

開発力で競っても絶対に勝てないので、pyserdeは以下の方針をとっています。

  • シリアライズ、デシリアライズの問題解決に集中する
  • ランタイムの型チェックはbeartypeに任せる
  • サードパーティーのパッケージのインテグレーションは、本体に入れずなるべくExtensionでやる

ただ、理由の1つ目に上げましたが、他の理由と比べたら全然大したことないと思っています。僕はやり方次第では個人のOSSでも企業が開発しているOSSに勝てるチャンスは全然あると思っています。

理由2: 広報活動

OSSで世界と戦うためにで使ってもらうことの重要性が言及されていましたが、激しく同意します。自分はインフルエンサーではないし、勉強会やカンファレンスで登壇することも滅多にないので、純粋にGithubで見つけてくれて好きになってくれた人たちだけ使ってくれていました。

2023年のある時、朝起きたらGithubスターが30ぐらい増えていたので何事かと思って調べたら、pyserdeにもコントリビュートしてくれており、RustのコンパイラパフォーマンスWGに所属しているKobzolことJakub Beránek氏が書いたWriting Python like it's Rustというブログ記事がHacker Newsの2023年5月21日のランキングで2位になっており、その記事のなかでpyserdeのことが言及されていました。Kobzol氏の記事はZennで日本語訳「Writing Python like it’s Rustの紹介・邦訳」があるので参照ください。

Recently I found an awesome serialization library called pyserde, which is based on the venerable Rust serde serialization framework.

結果、Hacker News効果で数日で数百スターぐらい伸び、利用者、コントリビューターが増えるきっかけになりました。pyserdeの場合はたまたま影響力のある人に使われて、ブログがバズったんですが、本来ならプロダクトを作る情熱と同じかそれ以上に自分の作ったものをブログや勉強会で発信していくことが重要です。

理由3: Rust

おそらくこれが最大の理由かもしれません。筆者がPythonを使い始めたのは2008年頃からで、速度が求められるサービスはC++で、それ以外のCLIツール、スクリプティング、Web開発、データエンジニアリング等はPythonを使ってきました。2017年からRustを使い始め、だんだんRustが手に馴染むようになってきたのと、Rustのエコシステムが急速に発展したことによって、2024年現在ではデータエンジニアリングと一部のスクリプティング用途を除き、Rustを使うようになってしまいました。

  • 2017年以前
    • HTTP以外のサービス: C++
    • WebAPI: Python
    • CLI: Python
    • スクリプティング: Python
    • データエンジニアリング: Python
  • 2024年現在
    • HTTP以外のサービス: Rust
    • WebAPI: Rust
    • CLI: Rust
    • IaC: TypeScript
    • スクリプティング: Rust or Python
    • データエンジニアリング: Python

Pythonをメインに使わなくなった影響はかなり大きくて、これまで業務でpyserdeを使うにあたってほしい機能を自分の余暇に実装したりしてたのが、ほぼ使うことが無くなってしまったので、僕個人のpyserdeに対する重要なフィードバックループが完全に途絶えてしまいました。また、Pythonを使う機会が減ると、少しずつ興味も薄れていきますし、最新機能へのキャッチアップも遅れてきます。

常に作っているOSSのユーザーであり続け、自らドッグフーディングするのが大変重要です。

ここまでやってきて良かったこと・学んだこと

ドキュメントを英語でちゃんと書く

よく言われていることですが、やはりドキュメンテーションは最重要です。

pyserdeでは以下の3つのドキュメントを作っています。

  • README: 概要、機能一覧、ライセンス、コントリビュータ一覧等
  • Guide: 主にチュートリアルや機能の説明等。Rust使いなのでmdBookで作っています。
  • API documentation: pdocを使って作っています。見ている人は少ないかも

筆者はドキュメンテーションよりプログラミングのほうが圧倒的に好きなので、ドキュメント書くのはかなりつらいです 😅 でも大事なので頑張っています。

とことん自動化

面倒だとやらなくなるのでとことん自動化する

  • CIでユニットテスト、Linter、スタイルチェック、コードカバレッジ
  • Github Releaseでリリースノート生成、PyPIでパッケージ公開、ドキュメントの公開
    • .github/release.ymlを用意してPRのラベルからリリースノートを自動生成[1]
    • poetry-dynamic-versioningを使ってGitタグからpythonパッケージのバージョンを動的に反映、パッケージをビルドする[2]

全部自分でやらない

細かい変更とか全部自分でやりたくなっちゃいますが、全部自分でやらなくていいです。むしろあえて他の人のために残しておくと良いです。何かのOSSにコントリビュートしたいと思っている開発者は世界中にごまんといて、彼らもコントリビュートできるし、OSS側もコントリビュータを増やせるのでwin-winです。

good first issueラベルを付けておくと彼らが探しやすくなるのでいいでしょう。

でも何でもいれたらいいというわけではない

コントリビュートしてくれるのは大変嬉しいですが、プロジェクトの方針に合わない場合もあります。その場合はちゃんと断りましょう。過去になんとなく変更を入れてしまってテストが不十分だったり、あとで意図が分からなくなってしまったことがありました。

思い切ってある程度の互換性は捨てる

開発リソースが限られているので、ある程度割り切って互換性を捨てることもしています。例えば、Python 3.8サポートをEOLよりも数ヶ月前に廃止しました[3]。理由はPython 3.8のtypingモジュール周りの乖離を吸収するコードをメンテし続けるよりは、思いきって削除してより綺麗なコードベースにしたほうが、開発生産性もやる気もあがると思ったからです。

機能毎に超簡単なexampleを作る

機能毎に超簡単なexampleを作っておくと、リグレッションテストにもなるし、機能のshowcaseにもなるのでおすすめ。

alias.py any.py class_var.py collection.py custom_class_serializer.py
custom_field_serializer.py custom_legacy_class_serializer.py default_dict.py default.py enum34.py
env.py field_order.py flatten.py forward_reference.py frozen_set.py
generics_nested.py generics_pep698.py generics.py global_custom_class_serializer.py init_var.py
jsonfile.py kw_only.py lazy_type_evaluation.py literal.py msg_pack.py
nested.py newtype.py pep681.py plain_dataclass_class_attribute.py plain_dataclass.py
primitive_subclass.py python_pickle.py recursive_list.py recursive.py recursive_union.py
rename_all.py rename.py simple.py skip.py tomlfile.py
type_check_coerce.py type_check_disabled.py type_datetime.py type_decimal.py type_ipaddress.py
type_numpy.py type_pathlib.py type_uuid.py union_directly.py union.py
union_tagging.py user_exception.py variable_length_tuple.py yamlfile.py

自作OSSを業務で使っていく

可能であれば自分の業務でどんどん使っていくことをおすすめします。業務で使うと足りない機能や問題点がつかみ易いです。

無理をしない

過去に家族との時間を犠牲にしてまでOSS開発していたことがありました。自分の妻や子供の人生の方が自分のキャリアより大事だということに気が付き、家族優先で空いた時間にマイペースにやるようにしています。

おわりに

ここまで読んでくれてありがとうございます!
せっかくpyserdeのことについて知ってもらえたので、良ければ使ってみてください!

また、コントリビュートしたいんだけど何したらいい?ここの設計どうなってんの?みたいな質問でもいいし、その他、相談、質問、コメントがあったら以下にどうぞ。

脚注
  1. GitHub Actionsでいい感じのリリースノートを完全自動で作成する ↩︎

  2. poetryを利用した動的なバージョン管理とGitHub ActionsによるPyPIへのrelease ↩︎

  3. Drop python 3.8 and use PEP585 entirely #503 ↩︎

GitHubで編集を提案

Discussion