PuLPは、線形計画問題と混合整数計画問題をPythonのコードで書き下すためのモデリングライブラリです。解を計算するのはPuLP自身ではなく、CBCやHiGHSといった外部のソルバーです。2026年9月25日に公開されたPuLP 4.0.0で書き方が大きく変わり、Web上の入門記事にある LpVariable("x", lowBound=0) や LpStatus[prob.status] は、現行の4.2.0ではエラーで止まります。この記事では、4.2.0で実際に動かしたコードと出力をもとに、インストールから求解、3.xからの移行までを順に説明します。
まとめ
PuLPは問題を記述する側(モデラー)で、計算する側(ソルバー)は別に入れます。現行の安定版は4.2.0(2026年10月22日公開)で、Python 3.12以上が必要です。Python 3.10・3.11で pip install pulp を打つと3.3.2が入り、書き方が違う版になります。
4.2.0は pip install pulp だけではソルバーが1つも入らず、さらにnumpyが無いと import pulp の時点で失敗します。pip install "pulp[highs]" か、pip install "pulp[cbc]" numpy で入れてください。
書き方は、LpProblem で問題を作り、prob.add_variable() で変数を作り、目的関数と制約を += で足し、solve() を呼ぶ順です。solve() は LpSolveStats を返し、状態は stats.status_str で読みます。制限時間で打ち切ったときは3.x のように「Optimal」とは返らず TimeLimit になるため、解の有無は stats.has_solution で判定します。
PuLPが担う役割とソルバーとの分担
PuLPが行うのは、Pythonで書いた数式をソルバーが扱えるモデルに変換して渡し、返ってきた解を変数に書き戻すことです。受け渡しにはMPS・LPファイルを使う方式と、Python API経由でメモリ上のモデルを渡す方式があります。CBCを使う COIN_CMD の場合、一時ディレクトリに .mps ファイルを書き出し、それを引数に cbc の実行ファイルを起動します。4.0からはモデルの中身をRustで書かれた拡張モジュール(pulp._rustcore)が保持する構造に変わりましたが、モデラーとソルバーの分担は変わっていません。
この分担のおかげで、無償のCBCやHiGHSで足りなくなったら、solve() に渡すソルバーをGurobiやCPLEXに変えるだけで移れます。裏を返すと、ソルバーが見つからなければPuLP側の文法が正しくても実行は失敗します。
扱えるのは、目的関数と制約がすべて一次式で書ける問題です。変数どうしの掛け算や二乗が入った時点で対象外になります。変数・目的関数・制約の切り分け方は決定変数・目的関数・制約条件とは?違いと定式化の手順を例題で解説、最適化全体の用語は数理最適化とは?3要素・アルゴリズム・機械学習との違いをPythonで解説にまとめています。
PuLPのインストールとソルバーの入れ方
pip installで入るバージョンとPython要件
4.x系はすべて requires_python >=3.12 です(PyPIのPuLPページ)。pipはPythonの版に合う最新を選ぶため、同じコマンドでも環境によって入る版が変わります。Python 3.11の仮想環境で pip install pulp を実行すると、入ったのは3.3.2でした。
| バージョン | PyPI公開日 | 必要なPython | 主な変更 |
|---|---|---|---|
| 3.3.2 | 2026-05-25 | 3.10以上 | 3.x系の最終版 |
| 4.0.0 | 2026-09-25 | 3.12以上 | Rustコア・CBC非同梱 |
| 4.0.1 | 2026-10-14 | 3.12以上 | HiGHSのモデル構築を一括化 |
| 4.1.0 | 2026-10-19 | 3.12以上 | highspyの版上限を撤廃 |
| 4.2.0 | 2026-10-22 | 3.12以上 | cat変更・列追加・制約編集 |
手元の版は import pulp; print(pulp.__version__) で確かめます。3.xにあった pulp.VERSION は4.0で削除されました。検索で見つかるサンプルの多くは3.x前提なので、エラーを調べる前に版を確かめてください。
extrasによるCBC・HiGHSの追加
4.0.0でCBCのバイナリ同梱が廃止され、ソルバーはpipのextrasで選んで入れる形になりました(PuLPのHISTORY)。4.2.0で素の pip install pulp だけを入れた仮想環境では、次の2点で止まります。
$ pip install pulp
$ python -c "import pulp"
File ".../pulp/apis/highs.py", line 11, in <module>
import numpy as np
ModuleNotFoundError: No module named 'numpy'
$ pip install numpy
$ python -c "import pulp; print(pulp.listSolvers(onlyAvailable=True))"
[]
1つ目は、HiGHS用のアダプターがimport時にnumpyを読み込むのに、PuLPの必須依存にnumpyが宣言されていないために起きます。4.0.1から4.2.0まで同じ状態です。2つ目は、使えるソルバーが0本という意味です。pulp[cbc] だけを入れた場合もnumpyのエラーは同じで、numpy を足すと ['COIN_CMD'] が返りました。pulp[highs] はhighspyがnumpyを連れてくるため、そのまま ['HiGHS'] が使えます。
pip install "pulp[highs]" # HiGHS(numpyも入る)
pip install "pulp[cbc]" numpy # CBC(cbcboxがcbc実行ファイルを置く)
4.2.0のextrasは14種類あります。無償のものは cbc・highs・highs-cmd・scip・glpk・cylp・ortools と、無償系をまとめた open-py(highspy・PySCIPOpt・cbcbox・swiglpk)です。商用は gurobi・cplex・xpress・copt・mosek と、まとめ指定の public-py で、pipパッケージに含まれる実行機能とライセンス条件は製品ごとに異なります。例えばgurobipyには小規模モデル用の制限付きライセンスが含まれますが、用途や規模によって別途ライセンスの取得が必要です。
pulp[cbc] で入るcbcbox 2.929のCBCは、起動すると Version: Devel (unstable)、Build Date: Mar 23 2026 と表示する開発版ビルドです。cbcboxは仮想環境の bin に cbc を置くので、仮想環境を有効化せずにPythonを直接叩くとPATHに cbc が無く、COIN_CMD が使えません。
PuLP 4.2で最小化問題を解く手順
LpProblem・add_variable・目的関数・制約の記述順
4.xでは変数を問題から作るため、先に LpProblem を作るのが3.xとの最大の違いです。手順は、問題を作る、prob.add_variable() で変数を作る、目的関数と制約を += で足す、solve() を呼ぶ、の4つです。比較演算子を含まない式は目的関数、含む式は制約として扱われます。
import pulp
prob = pulp.LpProblem("mincost", pulp.LpMinimize)
x = prob.add_variable("x", lowBound=0)
y = prob.add_variable("y", lowBound=0)
prob += 3 * x + 5 * y, "cost"
prob += x + y >= 10, "demand"
prob += x <= 6, "cap_x"
stats = prob.solve(pulp.COIN_CMD(msg=False))
print(stats.status_str, stats.objective, x.varValue, y.varValue)
# Optimal 38.0 6.0 4.0
単価3のxを上限の6まで使い、残りの4を単価5のyで埋めた結果、総コストは38です。+= の右の文字列は目的関数や制約の名前です。名前を省略した制約には連番が振られますが、目的関数の名前は未指定のままです。lowBound を省略した変数は下限が -inf になり、負の値も取ります。SciPyの linprog は既定で変数を0以上に制限するので、両方を使う人ほど付け忘れに注意してください。
add_variable_dictsとlpSumによる多変数モデルの構築
倉庫と店舗の組み合わせのように変数が添字で並ぶときは、prob.add_variable_dicts() で辞書としてまとめて作り、lpSum() で合計します。3.xの LpVariable.dicts() の後継で、引数の並び(名前・添字・下限・上限・cat)は同じです。
import pulp
supply = {"東京": 50, "大阪": 40}
demand = {"A店": 30, "B店": 25, "C店": 35}
cost = {("東京", "A店"): 4, ("東京", "B店"): 6, ("東京", "C店"): 9,
("大阪", "A店"): 5, ("大阪", "B店"): 3, ("大阪", "C店"): 4}
prob = pulp.LpProblem("transport", pulp.LpMinimize)
x = prob.add_variable_dicts("x", (supply, demand), lowBound=0, cat=pulp.LpInteger)
prob += pulp.lpSum(cost[w, s] * x[w][s] for w in supply for s in demand)
for w in supply:
prob += pulp.lpSum(x[w][s] for s in demand) <= supply[w], f"supply_{w}"
for s in demand:
prob += pulp.lpSum(x[w][s] for w in supply) == demand[s], f"demand_{s}"
stats = prob.solve(pulp.HiGHS(msg=False))
print(stats.status_str, stats.objective)
for w in supply:
print(w, {s: x[w][s].varValue for s in demand})
# Optimal 395.0
# 東京 {'A店': 30.0, 'B店': 20.0, 'C店': 0.0}
# 大阪 {'A店': 0.0, 'B店': 5.0, 'C店': 35.0}
添字に2つの辞書を渡すと x["東京"]["A店"] の形で引ける入れ子の辞書になります。制約は for ループで1本ずつ足し、名前にf文字列で添字を入れておくと、後から prob.get_constraint_by_name("supply_東京") で取り出せます。
合計に組み込みの sum() ではなく lpSum() を使うのは、構築時間が違うためです。変数2,000個の合計式を作る時間を、Python 3.13・Core i9-9880HのmacOSで3回測った最小値は次のとおりでした。4.xで sum() の遅さは大きく縮みましたが、それでも約10倍の差があります。
import pulp, time
prob = pulp.LpProblem("bench", pulp.LpMinimize)
xs = prob.add_variable_dicts("x", range(2000), lowBound=0) # 変数作成は計測の外
for f in (pulp.lpSum, sum):
ts = []
for _ in range(3):
t = time.perf_counter()
f(xs[i] for i in range(2000))
ts.append(time.perf_counter() - t)
print(f.__name__, f"{min(ts) * 1000:.2f} ms")
3.3.2では変数を LpVariable.dicts() で作る以外は同じコードで測りました。値は実行環境や負荷で変わります。
| 版 | lpSum() | sum() |
|---|---|---|
| PuLP 3.3.2 | 1.94ミリ秒 | 590.58ミリ秒 |
| PuLP 4.2.0 | 1.39ミリ秒 | 13.60ミリ秒 |
solve()が返すLpSolveStatsの読み方
4.xの solve() は整数ではなく LpSolveStats オブジェクトを返します。状態は stats.status_str(文字列)か stats.status(LpSolveStatus 列挙型)で読み、目的関数値は stats.objective でも pulp.value(prob.objective) でも取れます。変数の値は従来どおり varValue です。
状態の値は、Optimal(1)、NotSolved(0)、Infeasible(-1、制約を同時に満たす解が無い)、Unbounded(-2、目的関数がいくらでも良くなる)、Undefined(-3)に加え、4.0で TimeLimit(-4)から NumericalError(-12)までの停止理由が増えました。LpSolveStatus は整数と比較できるので stats.status == 1 も通りますが、3.xの "Not Solved" は "NotSolved" に表記が変わっています。文字列で比較していたコードは列挙型での比較に直してください(公式の移行ガイド)。
整数変数を使う混合整数計画(MILP)と制限時間
catの指定と作成後の変更
トラックの台数のように整数でなければ意味をなさない変数は、cat=pulp.LpInteger を指定します。0か1かを選ぶなら pulp.LpBinary です。4.2.0からは作成後に cat を書き換えられるようになったので、連続緩和と整数解を同じモデルで比べられます。
import pulp
prob = pulp.LpProblem("trucks", pulp.LpMinimize)
n = prob.add_variable("n", lowBound=0)
prob += n
prob += 8 * n >= 23
for cat in (pulp.LpContinuous, pulp.LpInteger):
n.cat = cat
stats = prob.solve(pulp.COIN_CMD(msg=False))
print(cat, stats.status_str, n.varValue)
# Continuous Optimal 2.875
# Integer Optimal 3.0
23トンの荷物を8トン積みで運ぶ問題です。連続変数のままだと2.875台が返ります。この例は切り上げれば3台で一致しますが、制約が複数絡むモデルでは、連続解の切り上げが最適にならないうえ制約違反になることもあります。整数であるべき変数には最初から LpInteger を指定してください。なお4.xでは LpBinary で作った変数の cat は "Integer" を返し、境界が0と1になります。二値かどうかは isBinary() で判定します。
制限時間で打ち切ったときのTimeLimitとhas_solution
整数変数が増えると求解時間が急に伸びるため、実務では COIN_CMD(timeLimit=60) のように制限時間を付けます。ここで3.xと4.xの振る舞いが変わりました。二値変数300個・制約30本のナップサック問題を、制限時間3秒で解いた結果です。
import pulp, random
random.seed(1)
n, m = 300, 30
prob = pulp.LpProblem("mkp", pulp.LpMaximize)
x = prob.add_variable_dicts("x", range(n), cat=pulp.LpBinary)
v = [random.randint(10, 100) for _ in range(n)]
prob += pulp.lpSum(v[i] * x[i] for i in range(n))
for j in range(m):
w = [random.randint(5, 60) for _ in range(n)]
prob += pulp.lpSum(w[i] * x[i] for i in range(n)) <= sum(w) // 2
stats = prob.solve(pulp.COIN_CMD(msg=False, timeLimit=3))
print(stats.status_str, stats.has_solution, stats.objective, stats.best_bound)
# 出力例(暫定解と上界は実行環境や実行ごとに変わる)
# TimeLimit True 11541.0 11578.985
4.2.0は TimeLimit を返し、解は持っている(has_solution が True)ものの最適性は証明されていない、と区別して教えてくれます。best_bound との差から、この解は最適値から最大で0.33%しか離れていないことも読めます。同じモデルを3.3.2の PULP_CBC_CMD(timeLimit=3) で解くと、戻り値は 1(Optimal)でした。最適性が証明されていないことは prob.sol_status の 2(Solution Found)にしか現れません。
このため、3.xで書いた if prob.status == LpStatusOptimal: を4.xで stats.status == LpSolveStatus.Optimal へ機械的に置き換えると、これまで採用していた時間切れの解がすべて失敗扱いになります。業務で「時間内に見つかった最良解を使う」設計なら stats.has_solution で判定し、「最適と証明された解だけを使う」なら Optimal で判定する、と意図に合わせて書き分けてください。需要予測の結果から発注量や生産量を決めるような業務組み込みでは、この判定がそのまま運用ルールになります。予測と最適化をつないだ仕組みづくりはAI予測分析開発・需要予測開発でも扱っています。
HiGHSとCBCの使い分けとソルバー指定
solve() を引数なしで呼んだときの既定ソルバーは、4.2.0のソースでは COIN_CMD(CBC)が使えればそれ、無ければ GLPK_CMD だけが候補です。HiGHSは自動では選ばれません。pulp[highs] だけを入れた環境で prob.solve() を呼ぶと、listSolvers() には HiGHS が出ているのに PulpError: No solver available で止まりました。HiGHSを使うときは prob.solve(pulp.HiGHS(msg=False)) と明示します。
| ソルバー | PuLPでの指定 | extras | ライセンス |
|---|---|---|---|
| CBC | COIN_CMD | cbc | EPL-2.0 |
| HiGHS | HiGHS | highs | MIT |
| SCIP | SCIP_PY | scip | Apache-2.0 |
| Gurobi | GUROBI | gurobi | 商用 |
| CPLEX | CPLEX_PY | cplex | 商用 |
使い分けの判断は単純です。CBCを外部プロセスで呼ぶ COIN_CMD は、MPSファイルの書き出しと起動の手間がかかる代わりに、引数なしの solve() で動きます。HiGHSはhighspy経由でメモリ上のモデルを直接渡し、4.0.1からはモデルの列と行をそれぞれ1回の呼び出しで組み立てる方式に変わりました。新しく書くならHiGHSを明示する、既存の PULP_CBC_CMD を置き換えるだけなら COIN_CMD にする、が手戻りの少ない選び方です。
ライセンスにも差があります。PuLP本体とHiGHSはMITですが、CBCはEclipse Public License 2.0です(CBCのLICENSE)。自社サーバー内で解くだけなら通常は問題になりませんが、CBCのバイナリを自社製品に同梱して配る場合はEPL-2.0のソース提供義務を確認してください。MITで揃えたいならHiGHSを選びます(HiGHSのLICENSE)。SCIPをPuLPから使う手順と性能はSCIPソルバーとは?PySCIPOptでPythonから使う手順とCBC・HiGHSとの比較、商用ソルバーの価格はGurobiとは?Pythonでの使い方・インストール・ライセンスと価格で扱っています。
PuLP 3.xから4.xへのコード移行
4.xで動かなくなる書き方とエラーメッセージ
3.xの書き方を4.2.0で実行すると、次のエラーになります。検索で見つけたサンプルが動かないときは、まずこの表と突き合わせてください。
| 3.xの書き方 | 4.2.0での結果 | 4.xの書き方 |
|---|---|---|
| LpVariable(“x”, lowBound=0) | TypeError | prob.add_variable(“x”, lowBound=0) |
| LpVariable.dicts(…) | AttributeError | prob.add_variable_dicts(…) |
| LpStatus[prob.status] | AttributeError | stats.status_str |
| PULP_CBC_CMD(msg=False) | AttributeError | COIN_CMD(msg=False) |
| prob.constraints[“c1”] | TypeError | prob.get_constraint_by_name(“c1”) |
| pulp.VERSION | AttributeError | pulp.__version__ |
実際のメッセージは次のとおりです。LpVariable のエラーは引数名を名指しするので、「lowBoundという引数は無い」と読めてしまい原因を取り違えがちです。直接生成そのものが廃止された、と読んでください。
TypeError: LpVariable.__init__() got an unexpected keyword argument 'lowBound'
AttributeError: type object 'LpVariable' has no attribute 'dicts'
AttributeError: module 'pulp' has no attribute 'LpStatus'
AttributeError: module 'pulp' has no attribute 'PULP_CBC_CMD'
TypeError: 'method' object is not subscriptable
3.3.2を経由する移行手順
公式の移行ガイドが勧める順序は、先に3.3.2で警告を潰してから4.xへ上げる方法です。3.3.1と3.3.2は、4.0で消える呼び出しの大半に DeprecationWarning を出し、add_variable などの新しいメソッドもすでに持っています。
pip install "pulp==3.3.2"
python -W error::DeprecationWarning my_model.py # 警告を例外にして洗い出す
警告を全部直すと、モデルを組む部分は3.3.2でも4.xでも動くコードになります。残るのは solve() の戻り値を読む部分で、ここだけは両方の版で動く書き方がありません。4.xへ上げてから、前の章の has_solution と Optimal の書き分けを決めて直します。3.3.2で警告を止めるために使っていた set_v4_migration_warnings(False) は4.xに存在しないので、呼び出しごと消してください。
移行で見落としやすい挙動の変化
エラーにならずに結果だけが変わる、あるいは設計の変更を迫られる点が3つあります。4.2.0で確かめた挙動です。
- 変数は作った問題でしか使えない:問題Aの変数を問題Bの制約に入れると
PulpError: Expression is bound to a different model than the one receiving the constraint.になります。同じ変数で複数の問題を組んでいたコードは、問題ごとに変数を作り直します。 - 問題をpickleできない:
pickle.dumps(prob)はTypeError: cannot pickle 'builtins.Model' objectです。multiprocessingにモデルを渡していた処理は、prob.toDict()で辞書にして渡し、受け側でLpProblem.fromDict()で組み直します。 - 制約の式を後から書き換えても問題に届かない:問題は制約のコピーを持つため、元の式を変更しても反映されません。
prob.addConstraint()が返す制約か、get_constraint_by_name()で取り出した制約を編集します。
3.xのElastic制約(makeElasticSubProblem)も削除されました。制約違反を許してペナルティを課したい場合は、非負のスラック変数を制約に足し、目的関数にその重みを加える形で明示的に書きます。
PuLPを選ばない方がよい条件
目的関数や制約に二次以上の項が入る非線形問題は、PuLPでは書けません。無理に線形化するより、Pythonの最適化ライブラリscipy.optimizeの使い方で扱う scipy.optimize が候補になります。ただし、連続変数の非線形最適化と、整数条件を含む非線形最適化では必要な手法が異なります。
シフト表や配車で、時間区間の重複禁止や「AならばB」といった論理制約を大量に書きたい場合は、OR-Toolsを先に検討してください。単純な重複割当の禁止なら二値変数の和を1以下にする制約でPuLPでも書けますが、条件分岐を大きな定数(Big-M)で表すと、定数の設定次第で求解性能が悪化することがあります。制約プログラミングのCP-SATを持つGoogle OR-Toolsとは?Pythonでの使い方とインストール、CP-SAT・配送ルートの実行例では、こうした制約を専用APIで記述できます。求解速度は問題と設定に依存するため、実際のモデルで比べてください。OR-Tools内でソルバーを切り替えて使うならOR-Tools MathOptとは?pywraplpとの違い・対応ソルバーとPythonでの使い方が選択肢になります。
逆に、線形または混合整数の問題を、pandasなどのデータ処理と同じPythonコードの中で書きたいなら、数式に近い記法でモデルを書けるPuLPが合っています。ツール全体の比較はORツールとは?OR-Tools・Gurobiなど主要6ツールの比較と選び方にまとめています。
よくある質問
PuLPの最新バージョンはいくつですか?
2026年10月26日時点の安定版は4.2.0(2026年10月22日公開)で、Python 3.12以上が必要です。4.0.0からひと月足らずで4.0.1・4.1.0・4.2.0と更新が続いているため、最新は PyPIのPuLPページ で確かめてください。手元の版は pulp.__version__ で確認できます。
LpVariable.dictsは4.xでも使えますか?
使えません。4.xでは AttributeError: type object 'LpVariable' has no attribute 'dicts' になります。後継は prob.add_variable_dicts() で、名前・添字・下限・上限・catの引数はそのまま渡せます。問題を先に作っておく必要がある点だけ注意してください。
LpProblem() の引数には何を渡しますか?
第1引数がモデル名、第2引数(sense)が最適化の向きで、pulp.LpMinimize(最小化)か pulp.LpMaximize(最大化)を渡します。省略時はモデル名が "NoName"、向きは最小化です。モデル名に空白を入れると Spaces are not permitted in the name. Converted to '_' という警告が出てアンダースコアに置き換わるので、"mincost" のように空白なしで付けてください。
ソルバーを指定せずに solve() を呼ぶと何が使われますか?
4.2.0では、CBC(COIN_CMD)が使えればCBC、無ければGLPK(GLPK_CMD)です。どちらも無いと PulpError: No solver available で止まり、HiGHSだけを入れた環境でもこのエラーになります。その環境で使えるソルバーは pulp.listSolvers(onlyAvailable=True) で一覧でき、HiGHSは solve(pulp.HiGHS()) と明示して使います。
PuLPで非線形の最適化問題は解けますか?
解けません。PuLPが扱えるのは、目的関数と制約がすべて一次式で書ける線形計画と、そこに整数条件を加えた混合整数計画までです。変数どうしの積や二乗、対数が入る問題には scipy.optimize や非線形に対応したソルバーを使ってください。