Library / SDK
qpsolvers/qpsolvers avatar
qpsolvers/qpsolvers

qpsolvers: one solve_qp call across twenty quadratic programming backends

Quadratic programming solvers in Python with a unified API

763 stars105 forksPythonLGPL-3.0

At a glance

What is it?
qpsolvers is a Python library that puts a single solve_qp interface in front of roughly twenty quadratic programming backends, from commercial ones to open source ones. It suits teams swapping solver vendors or supporting several environments, and it inherits every convexity requirement and failure mode of the backend they pick.
Who is it for?
qpsolvers is the right choice for a Python codebase that must run its quadratic programs on more than one backend, or that cannot commit to a commercial solver licence, and its wheels_only extra covers environments where building from source fails. It is not the right choice for a nonconvex objective or for a team that needs certified duals, because it returns None on failure and leaves dual details to the backend.
Can I use it commercially?
Yes, with conditions. LGPL-3.0 is a weak copyleft licence: you can use it inside commercial and closed-source software, but if you distribute changes to its own files, you must publish those changes under the same licence.
Is it still maintained?
Yes. The repository received new commits within the last day.
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 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One solve_qp signature over roughly twenty backends

Quadratic programming shows up whenever a control, planning or allocation problem has a quadratic objective and linear constraints. The difficulty is not writing the formulation but that every solver package exposes a different calling convention: different matrices, different inequality conventions, different flags for warm starts and tolerances.

qpsolvers removes that layer. The library exposes a solve_qp function that solves the standard convex form with a quadratic cost, inequality constraints G x <= h, equality constraints A x = b and box bounds lb <= x <= ub, with vector inequalities applied coordinate by coordinate. The backend is chosen with a solver keyword argument rather than by importing a different package and learning its API.

The table of supported solvers lists entries from Clarabel, DAQP, ECOS, HiGHS, HPIPM, jaxopt.OSQP, KVXOPT, OSQP, PDHCG, PIQP, ProxQP, QPALM, qpmad, QTQP, qpax, qpOASES, qpSWIFT and others, plus commercial ones such as COPT, Gurobi, MOSEK and NPPro. Each row records the algorithm family and whether the backend's own API handles dense matrices, sparse matrices, or both.

The audience is Python users who need portability across solver vendors, whether because a customer environment forbids a commercial licence or because a production system must fall back to a lighter backend.

Convexity is a contract you cannot negotiate with the backend

Every backend in the table expects a convex problem, meaning the matrix P should be positive semi-definite, and some solvers go further and require strict convexity with P positive definite. That requirement is not enforced by qpsolvers itself. The library passes your matrices to the backend and returns what comes back.

The failure signal is the return value. solve_qp returns the primal solution found by the backend solver, or None in case of failure or an unfeasible problem. That is a coarse contract. A None tells you the solve did not produce a solution, but it does not separate infeasibility from a numerical breakdown, and it does not tell you which constraint was violated.

If your problem is genuinely nonconvex, no choice of solver keyword changes the outcome, because the convexity assumption is shared by all of them. The same applies to a P that is only semidefinite if you select a backend that insists on positive definiteness: the same problem can solve under one keyword and return None under another. If your objective has a semidefinite Hessian and you care about the solution, treat the backend choice as part of the problem specification rather than a performance knob.

Installing only the backends you can actually run, then solving a first QP

The package lives on PyPI and on conda-forge. The conda-forge route is a single command:

console
conda install -c conda-forge qpsolvers

From PyPI, the documented one-size-fits-all install pulls the open source solver set:

console
pip install qpsolvers[open_source_solvers]

The documentation warns that this variant may not work immediately on all systems, for instance if a solver tries to compile from source. Narrower variants exist for that case: one that installs only solvers with pre-compiled binaries, one that installs a named set such as clarabel, daqp, proxqp and scs, and one that installs the library with no solver at all.

The per-solver extras are declared in the packaging metadata, one entry per backend, and the library core itself depends only on numpy and scipy with Python 3.10 as the floor.

On import, qpsolvers loads all the solvers it can find and lists them in qpsolvers.available_solvers. That attribute is worth checking at startup rather than assuming a backend is present, since a missing package simply means the keyword is unavailable.

The README example builds the matrices and passes them positionally, with the backend named by keyword. The Hessian is formed as M.T @ M, which makes it positive definite by construction and therefore acceptable to every backend:

python
import numpy as np
from qpsolvers import solve_qp

M = np.array([[1.0, 2.0, 0.0], [-8.0, 3.0, 2.0], [0.0, 1.0, 1.0]])
P = M.T @ M  # this is a positive definite matrix
q = np.array([3.0, 2.0, 3.0]) @ M
G = np.array([[1.0, 2.0, 1.0], [2.0, 0.0, 1.0], [-1.0, 2.0, -1.0]])
h = np.array([3.0, 2.0, -2.0])
A = np.array([1.0, 1.0, 1.0])
b = np.array([1.0])

x = solve_qp(P, q, G, h, A, b, solver="proxqp")
print(f"QP solution: {x = }")

The documented output for this problem is the vector [0.30769231, -0.69230769, 1.38461538]. The shape of the call is the contract worth remembering: objective first as P and q, then inequalities as G and h, then equalities as A and b, with the solver keyword last. Every backend reachable through the library accepts this same arrangement.

solve_problem when the dual multipliers matter

solve_qp returns a primal vector and nothing else, which is enough for many callers and insufficient for others. A control problem that needs to know which constraint is binding, or a unit test that checks complementary slackness, wants the dual multipliers.

That is what solve_problem is for. The library also documents a solve_ls function for (possibly constrained) least-squares problems, and solve_problem returns not only the primal solution but its dual multipliers and the other quantities the backend solver computed. The dual multipliers come from whichever backend you selected, so their sign conventions and scaling follow that solver's conventions rather than a normalised set of qpsolvers ones.

The repository carries example scripts for these paths, including dual_multipliers.py, lasso_regularization.py, constrained_linear_regression.py, model_predictive_control.py, least_squares.py and sparse_least_squares.py. They are a reasonable way to see what each backend populates before you rely on a particular field.

Sparse and dense are not interchangeable across backends

The API column in the solver table is the field most likely to break a port. Several backends are sparse only, several are dense only, and a handful accept both. Clarabel, ECOS, HiGHS, HPIPM, OSQP, QPALM and QTQP sit on one side of that split, DAQP, jaxopt.OSQP and CVXOPT on the other, while KVXOPT, PDHCG, PIQP and ProxQP take dense and sparse.

The consequence is that the sparse and dense variants of the same problem are not interchangeable across a fleet of backends. If your application builds a sparse problem, the pool of usable backends is smaller than the total listed, and a solver swap that works in the dense test can fail on the sparse path.

Algorithm family also changes what you are signing up for numerically. The table mixes interior point backends, active set backends, augmented Lagrangian ones such as OSQP, jaxopt.OSQP, ProxQP and QPALM, a primal-dual hybrid gradient solver in PDHCG, and a proximal interior point method in PIQP. These families differ in how they handle ill-conditioning and in how dense they make the linear algebra, and the table alone does not tell you which one suits your problem sizes.

Licences travel with the solver you pick, not with the wrapper

The library itself is under LGPL-3.0, and the packaging metadata classifies it as GNU Lesser General Public License v3 with the project marked Production/Stable for development status.

The backends carry their own terms, and the table lists them explicitly. Apache-2.0 covers Clarabel, OSQP, PDHCG, qpmad, QTQP and jaxopt.OSQP. MIT covers DAQP, HiGHS and qpax. BSD-2-Clause covers HPIPM, PIQP and ProxQP. GPL-3.0 covers CVXOPT, ECOS and KVXOPT, and LGPL-2.1 covers qpOASES, while QPALM is LGPL-3.0.

Then there are the commercial rows: COPT, Gurobi, MOSEK and NPPro. Those need a licence from their vendor and are installed through their own Python packages, such as gurobipy, coptpy and mosek in the optional dependency list.

The practical point is that choosing a backend inside qpsolvers can change the licence obligations of the software you ship, and the LGPL-3.0 wrapper does not neutralise the GPL-3.0 backends it can load. Check the licence row for every backend you enable before distributing a binary.

Where the unified API stops paying off

The library is at its weakest when you have exactly one solver, one problem class, and no reason to switch. In that case a direct SDK call gives you backend specific options the wrapper does not expose, and you avoid the layer entirely.

The difference in approach is worth spelling out. A direct call to a commercial solver SDK gives you its full feature surface, its own modelling layer, warm start control and its native reporting, plus a licence you must obtain. Going through qpsolvers gives you a stable signature across backends and a one line change of solver keyword, at the cost of that feature surface. qpsolvers is the abstraction you want when the backend is negotiable and the formulation is fixed.

It also stops short of modelling. There is no objective builder, no constraint expression language, no problem compilation step. You assemble matrices yourself. For a research codebase with a hand written Hessian, that is exactly right. For a system where the structure changes per request, the modelling work sits with you either way, and qpsolvers only standardises the last call.

Release cadence and the cost of an upgrade

The project ships from PyPI and conda-forge, with recent releases including v4.13.0 on 2026-07-19, v4.12.0 on 2026-05-08 and v4.11.0 on 2026-03-16. The repository is not archived, and the last push was on 2026-10-19.

Those numbers describe a project that is still being changed, which cuts both ways. A recent change means the solver list in the table can grow between versions, so a pipeline that pins qpsolvers can find a different set of backends available after an upgrade of a solver extra. Pinning solver packages separately is the safer habit when reproducibility matters.

On the documentation side, the repository keeps a doc/ directory, an examples/ directory and a CHANGELOG.md, and CI is split across workflow files for tests and for documentation. The examples include model_predictive_control.py and lasso_regularization.py, which cover the two families of problem where a unified QP interface usually gets its first real use.

Editorial conclusion

qpsolvers is the right choice for a Python codebase that must run its quadratic programs on more than one backend, or that cannot commit to a commercial solver licence, and its wheels_only extra covers environments where building from source fails. It is not the right choice for a nonconvex objective or for a team that needs certified duals, because it returns None on failure and leaves dual details to the backend. Before adopting it, run the same small program against every backend you plan to ship and compare solutions and timings, then confirm that the solver packages you need actually publish binaries for your target platforms.

Frequently asked questions

How do I install qpsolvers with the open source solvers?

Use `pip install qpsolvers[open_source_solvers]`, which pulls the listed set of open source backends, or `conda install -c conda-forge qpsolvers`. If a solver tries to compile from source, the documentation points to the wheels_only variant that installs only solvers with pre-compiled binaries.

How do I check which QP solvers are available in qpsolvers?

On import the library loads all the solvers it can find and lists them in `qpsolvers.available_solvers`. A solver you did not install simply will not appear there.

What does qpsolvers return when the quadratic program cannot be solved?

solve_qp returns None in case of failure or an unfeasible problem. All solvers require the problem to be convex with P positive semi-definite, and some require P positive definite.

How do I get the dual multipliers from a qpsolvers solution?

Use solve_problem instead of solve_qp, which returns the primal solution along with its dual multipliers and the other quantities computed by the backend solver. The dual_multipliers.py example in the examples directory shows the usage.

Official sources

  1. Issues
  2. License: LGPL-3.0
  3. qpsolvers/qpsolvers 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/qpsolvers-qpsolvers.svg)](https://hysenlabs.com/projects/qpsolvers-qpsolvers)

Was this useful?