Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

Paldita Munkres V2: Faster. Smarter. More Robust.

Paldita Munkres V2 badge

munkres

CI License Python Dependencies Typed Release Coverage

The Munkres (Hungarian) algorithm for the assignment problem: given a cost for every (worker, job) pair, find the one-to-one assignment with the lowest total cost. Pure Python, no dependencies, fully typed, thread-safe.

            Job A  Job B  Job C
Worker 0      4      1      3
Worker 1      2      0      5        ->   0->B (1)   1->A (2)   2->C (2)   total 5
Worker 2      3      2      2

Install

pip install munkres        # once 2.0 is on PyPI; until then:
pip install git+https://github.com/iameishit/Paldita-munkres

Needs Python 3.10+. numpy and pandas are optional: if you have them, arrays and DataFrames are accepted as input.

Quick start

from munkres import Munkres

cost = [[4, 1, 3],
        [2, 0, 5],
        [3, 2, 2]]

pairs = Munkres().compute(cost)
print(pairs)                                   # [(0, 1), (1, 0), (2, 2)]
print(sum(cost[r][c] for r, c in pairs))       # 5

compute returns (row, column) pairs sorted by row. Rectangular matrices work too: min(rows, columns) pairs are returned.

Forbidding a pairing

from munkres import DISALLOWED, Munkres

cost = [[4, DISALLOWED, 3],
        [2, 0, DISALLOWED],
        [3, 2, 2]]
print(Munkres().compute(cost))                 # [(0, 2), (1, 1), (2, 0)]

float("inf") means the same as DISALLOWED.

Maximising profit instead of minimising cost

from munkres import Munkres, make_cost_matrix

profit = [[5, 9, 1], [10, 3, 2], [8, 7, 4]]
pairs = Munkres().compute(make_cost_matrix(profit))
print(sum(profit[r][c] for r, c in pairs))     # 23

When there is no valid assignment

Impossible constraints raise immediately (they used to hang forever in 1.x), and the exception tells you why:

from munkres import DISALLOWED as D, Munkres, UnsolvableMatrix

try:
    Munkres().compute([[1, D, D], [1, D, D], [1, 2, 3]])
except UnsolvableMatrix as error:
    print(error)       # No complete assignment exists: rows [0, 1] can only be matched to columns [0] ...
    print(error.rows, error.cols)              # (0, 1) (0,)

numpy and pandas

import numpy as np
from munkres import Munkres

print(Munkres().compute(np.array([[4, 1, 3], [2, 0, 5], [3, 2, 2]])))

Inputs are never modified. Fraction and Decimal costs work as well.

More than compute()

A richer result: solve

from munkres import solve

result = solve([[4, 1, 3], [2, 0, 5]])         # 2 workers, 3 jobs
print(result.pairs)                            # ((0, 1), (1, 0))
print(result.total)                            # 3
print(result.unmatched_cols)                   # (2,)

maximize=True maximises (exactly, even for Decimal/Fraction). Gating (max_cost=, or min_profit= when maximising) refuses bad pairs and leaves rows unmatched instead, which is what object tracking needs:

from munkres import solve

cost = [[1, 9, 9], [9, 2, 9], [9, 9, 8]]
print(solve(cost, max_cost=3).pairs)           # ((0, 0), (1, 1))
profit = [[5, 9, 1], [10, 3, 2], [8, 7, 4]]
print(solve(profit, maximize=True).total)      # 23

With a pandas DataFrame you get your labels back:

import pandas as pd
from munkres import solve

frame = pd.DataFrame([[4, 1], [2, 0]], index=["w1", "w2"], columns=["jobA", "jobB"])
print(solve(frame).labelled())                 # [('w1', 'jobB'), ('w2', 'jobA')]

Why is there no answer? diagnose

from munkres import DISALLOWED as D, diagnose

problem = diagnose([[1, D, D], [1, D, D], [1, 2, 3]])
print(problem.rows, problem.cols)              # (0, 1) (0,)

Watch it think: trace

from munkres import solve

trace = solve([[4, 1, 3], [2, 0, 5], [3, 2, 2]], trace=True).trace
print(trace.to_text().splitlines()[0])         # Hungarian algorithm on a 3x3 matrix (10 steps)

trace.to_html() gives a static page you can open or share.

Drop-in for SciPy, and a matrix builder

from munkres import build_cost_matrix, linear_sum_assignment, solve

rows, cols = linear_sum_assignment([[4, 1, 3], [2, 0, 5], [3, 2, 2]])
print(rows, cols)                              # [0, 1, 2] [1, 0, 2]
workers, jobs = [(0, 0), (5, 5)], [(1, 1), (6, 4)]
cost = build_cost_matrix(workers, jobs, lambda w, j: abs(w[0] - j[0]) + abs(w[1] - j[1]))
print(solve(cost).pairs)                       # ((0, 0), (1, 1))

Analysing a problem

from munkres import bottleneck, counterfactual, k_best, shadow_prices, tolerance

m = [[4, 1, 3], [2, 0, 5], [3, 2, 2]]
print(shadow_prices(m).total)                  # 5
print(counterfactual(m, 0, 0))                 # 1
print(tolerance(m, 0, 1))                      # 1
print([a.total for a in k_best(m, 3)])         # [5, 6, 6]
print(bottleneck([[1, 5], [5, 9]]).pairs)      # ((0, 1), (1, 0))
  • shadow_prices: the LP duals, a certificate anyone can re-check that the answer is optimal.
  • counterfactual(m, i, j): what forcing a pair would cost. tolerance(m, i, j): how much a chosen pair's cost may rise before the answer changes.
  • k_best(m, k): the k best assignments (Murty's algorithm).
  • bottleneck(m): make the worst single pair as good as possible.
from munkres import soft_assignment, stable_matching, transport

men = {"A": ["x", "y", "z"], "B": ["y", "x", "z"], "C": ["x", "y", "z"]}
women = {"x": ["B", "A", "C"], "y": ["C", "A", "B"], "z": ["A", "B", "C"]}
print(stable_matching(men, women))             # {'A': 'z', 'B': 'x', 'C': 'y'}
plan = transport([3, 2], [2, 3], [[1, 5], [4, 2]])
print(plan.flows)                              # {(0, 0): 2, (0, 1): 1, (1, 1): 2}
print(plan.total_cost)                         # 11
print(round(soft_assignment([[0.0, 1.0, 1.0], [1.0, 0.0, 1.0], [1.0, 1.0, 0.0]], 0.05)[0][0], 3))  # 1.0
  • stable_matching: preferences instead of costs (Gale-Shapley, proposer-optimal).
  • transport: ship quantities from suppliers to consumers at least cost.
  • sinkhorn / soft_assignment: entropic optimal transport, a soft, differentiable matching.

Command line

munkres costs.csv --maximize --json
printf '4,1,3\n2,0,5\n3,2,2\n' | munkres - --trace

Matrix files are comma- or space-separated; an empty cell, D, x or inf forbids a pairing. python -m munkres with no arguments runs a built-in self-check.

Input rules

input result
ragged rows ValueError
NaN, -inf ValueError
a non-number cell TypeError
empty matrix (no rows or no columns) []
DISALLOWED, +inf pairing forbidden
impossible constraints UnsolvableMatrix (a ValueError) with a Hall's-theorem witness

Performance

Pure-Python O(n²·m). Measured on one machine: 200x200 in ~0.1 s, 500x500 in ~0.8 s, 1000x1000 in ~4 s (about 5-50x faster than munkres 1.1.4 on random matrices, and on par when almost all costs tie; numbers in docs/BENCHMARKS.md, reproducible with python tools/benchmark.py).

Benchmark results: munkres 1.1.4 vs munkres 2.x vs SciPy, solve time on a log scale

For very large or latency-critical problems use a compiled solver such as scipy.optimize.linear_sum_assignment; this library's niche is zero dependencies, DISALLOWED support, exact Decimal/Fraction arithmetic and clear errors. munkres.linear_sum_assignment has the same call signature, so switching either way is a one-line change.

Migrating from 1.x

2.0 keeps the Munkres().compute(), make_cost_matrix, print_matrix, DISALLOWED and UnsolvableMatrix API, and Munkres().compute() is not deprecated. What changed:

  • ragged matrices now raise ValueError (they used to be silently mis-solved);
  • NaN / -inf raise ValueError (they used to hang), +inf = DISALLOWED;
  • impossible matrices raise UnsolvableMatrix (they used to hang);
  • Python 3.10+ only; setup.py is gone (pyproject.toml).

See CHANGELOG.md for the full list.

What's new in 2

Faster, typed, thread-safe, and impossible matrices now raise a clear error immediately. New: solve() with gating, labels and traces, optimality certificates, k-best, bottleneck, stable matching, transportation, Sinkhorn and a munkres command. Full details: V2_RELEASE.txt and CHANGELOG.md. Documentation: docs/index.md.

Development

See CONTRIBUTING.md. In short: pip install -e . and tools/audit.sh.

Credits and license

Created by Brian M. Clapper (2008-2020); maintained and extended since 2026 by Eishit Nigam. Licensed under the Apache License 2.0; see NOTICE.

Metadata

Release files for munkres 2.0.0rc1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for munkres 2.0.0rc1
File Size Uploaded
munkres-2.0.0rc1.tar.gz 104.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for munkres 2.0.0rc1
File Interpreter ABI Platform
munkres-2.0.0rc1-py3-none-any.whl Python 3 none any Details

Total release size: 134.8 kB

Release files / munkres-2.0.0rc1.tar.gz

Download URL munkres-2.0.0rc1.tar.gz
Size 104.8 kB
Tags Source
SHA-256 checksum
How to use checksums
e50072280e9325d7381e02438ef3f591152811965927221fb8f9841a454163ab
BLAKE2b-256 checksum
How to use checksums
e340fa5a4e614babd51611e17bf200cf4c01042b37c5a2780db2df672c153d9f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Release files / munkres-2.0.0rc1-py3-none-any.whl

Download URL munkres-2.0.0rc1-py3-none-any.whl
Size 30.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
68cff8b93b8bda20422de599c15344a6d0d2c47ff2e6efd08b0e67c25b63d758
BLAKE2b-256 checksum
How to use checksums
c883a7242621063cbf28fd8af23a4e780698380fe47c4d84c2f78adde8e790ba
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Release history Release notifications | RSS feed

2.0.0

2 release files

This release

2.0.0rc1 This release

2 release files

1.1.4

2 release files

1.1.2

2 release files

1.1.1

2 release files

1.0.12

1 release file

1.0.11

2 release files

1.0.10

2 release files

1.0.9

1.0.8

2 release files

1.0.7

2 release files

1.0.6

2 release files

1.0.5.3

1.0.5.2

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page