Library / SDK
PythonOT/POT avatar
PythonOT/POT

POT's build floor for Cython is 0.23, its Makefile advertises a target that does not exist, and its remove target reinstalls first

POT : Python Optimal Transport

2,848 stars569 forksPythonMIT

At a glance

What is it?
POT is a large library of optimal transport solvers with backends for five array libraries, and the README is a wall of gallery links rather than prose. The engineering detail lives in the packaging: a pyproject file that declares only a build backend, a setup script that regex-scrapes the version out of a source file, and a Makefile whose uninstall path installs the package in order to delete it.
Who is it for?
POT fits a machine learning or signal processing project that needs a specific optimal transport formulation rather than one general solver, and the honest reason to read its feature list is that the formulations are genuinely distinct, from entropic and unbalanced through Gromov-Wasserstein and its fused, quantized and semi-relaxed variants. Install from a wheel rather than building, since the source path compiles Cython extensions and probes OpenMP support at build time.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 5, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The build requires Cython 0.23 or newer and NumPy 2.0 on modern Python

The pyproject file contains one section and it is not the project metadata. There is no name, no version and no dependency list; there is a build-system table and a backend pointer to setuptools. That is the whole file, so all packaging metadata still lives in the legacy setup script and its companion configuration. What the build table does pin is a set of floors that do not agree in age. Setuptools needs 42 or newer, Cython needs 0.23 or newer, and NumPy is split by interpreter: below Python 3.9 the requirement resolves to the oldest supported NumPy package, while 3.9 and above gets NumPy 2.0.0 or newer. A Cython floor from the middle of the 2010s sitting next to a NumPy floor from 2024 is what a project with a long legacy tail looks like in four lines.

Optimisation level and C++ standard both change by operating system

The build script branches on the platform three times, and each branch does something different. The base optimisation argument is /O2 on Windows and -O3 everywhere else, so the two most common platforms are not compiled at the same level. Windows then gets /std:c++17 appended, annotated as needed for one module, and macOS gets -std=c++17 with a comment saying the same thing. On macOS the script additionally shells out to xcrun for the SDK path and rewrites the CFLAGS environment variable to point at that SDK root, then adds OpenMP linker flags. Linux receives neither an explicit C++ standard nor an SDK path, so the standard it compiles against is whatever the compiler defaults to. The OpenMP flags themselves are conditional on a probe run at build time.

The version is scraped out of a source file at build time

There is no static version string in the packaging metadata. The build script opens ot/__init__.py, runs a regular expression over its contents looking for the version assignment, and takes the first capture group. The comments around it are unusually self-aware, describing the approach as dirty but working, noting that the pattern also excludes a trailing inline comment, and adding that there is no need to check exceptions because a failure is allowed to let the build process fail loudly before a release. Two more build-time details sit alongside it. The script imports NumPy and Cython at module scope rather than deferring them, which is why both appear in the build requirements. And it appends the package's own helpers directory to the import path so it can call an OpenMP support check from there.

The Makefile help text lists eight targets and one of them is not defined

The help target prints its own list of available targets: help, build, install, sinstall, remove, sremove, clean and notebook. Reading further down the same file, the targets actually defined are build, buildext, install, sinstall, remove, sremove, clean, pep8, test, pytest, release, release_test and rdoc. So notebook is advertised and never defined, while five targets that do exist, including the test suite and the release upload, are not mentioned. A second loose end sits at the top of the file, where a variable reads the current branch name out of git symbolic-ref and is then never referenced by any target. Neither is harmful, but both are the kind of drift that makes a help target untrustworthy.

The remove target installs the package and then deletes what it installed

The two uninstall targets work in a way worth reading twice. To remove a local installation, the make target invokes the setup script with an install command and a record file, so it performs a fresh install while writing a list of the files it placed. Only then does it convert that list, turning newlines into nulls so paths with spaces survive, and pipe it through xargs with rm and a forced flag. The system-wide variant does the same thing through sudo. So both paths re-run an install in order to discover what to delete, rather than recording an install at the time it happened. It works because the recorded file list is deterministic, but it means an uninstall touches every installed path before removing it.

Tests run the linter first and execute docstrings as test cases

The test target depends on the pep8 target, so a style violation stops the suite before a single solver runs. That target invokes flake8 across the examples, the package and the tests, with a line length limit of 127, a counter, statistics and source display. The suite itself runs pytest with the twenty slowest durations reported, verbose output, the doctest-modules flag so that docstrings are executed as tests, and the GPU subpackage ignored. A second target named pytest runs the identical command without the lint dependency, which is the one to reach for when you want results without reformatting first. The ignore of the GPU directory means hardware-specific code is not exercised by the default path at all.

Every feature bullet is a gallery link and one of them points at a .hmtl file

The implemented features section is thirty-odd bullets, and the pattern is identical in each: a name, a bracketed reference number, and a link to a gallery example page rather than a paragraph of explanation. The reference numbers climb as high as eighty-two, and no bibliography appears in the file, so the citations cannot be resolved from the README alone. Two details stand out. One link ends in a file extension of .hmtl rather than .html, which is a typo in a URL that will not resolve. Another entry, for non-regularized Wasserstein barycenters, is annotated as being for small scale only, which is the single honest performance caveat in the whole list. The header badges are similarly untidy, with the conda-forge badge appearing twice in the same row.

Editorial conclusion

POT fits a machine learning or signal processing project that needs a specific optimal transport formulation rather than one general solver, and the honest reason to read its feature list is that the formulations are genuinely distinct, from entropic and unbalanced through Gromov-Wasserstein and its fused, quantized and semi-relaxed variants. Install from a wheel rather than building, since the source path compiles Cython extensions and probes OpenMP support at build time. Pick the backend you already use, expect docstrings to run as tests, and remember that the test target fails on lint before it fails on a broken solver.

Frequently asked questions

What is POT used for in machine learning?

It provides solvers for optimal transport problems aimed at signal processing, image processing and machine learning, including domain adaptation, optimal transport mapping estimation, subspace learning and graph neural network layers, plus barycenters and unbalanced and partial formulations.

Which array backends does POT support?

Five are named: PyTorch, JAX, TensorFlow, NumPy and CuPy. The backend list appears in the feature summary and again in the implemented features section, in a different order each time.

Does POT need to be compiled when installed from source?

The build requirements include Cython, and the setup script imports Cython and NumPy at module scope and calls an OpenMP support probe from the package's own helpers directory. It also adjusts optimisation flags per platform and forces a C++17 flag on Windows and macOS.

Where does the POT version number come from?

Not from the packaging metadata, which contains only a build-system table. The setup script reads ot/__init__.py and extracts the version with a regular expression at build time, with a comment saying a failure is allowed to break the build before a release.

How does the POT test suite run?

The make test target depends on a flake8 pass over the examples, package and tests with a 127 character limit, then runs pytest with verbose output, the slowest durations reported, doctests enabled, and the GPU subpackage ignored. A separate pytest target skips the lint step.

Official sources

  1. License: MIT
  2. Project website
  3. PythonOT/POT on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/pythonot-pot.svg)](https://hysenlabs.com/projects/pythonot-pot)