Library / SDK
PyPortfolio/PyPortfolioOpt avatar
PyPortfolio/PyPortfolioOpt

PyPortfolioOpt: mean-variance, Black-Litterman and HRP in one Python library

Financial portfolio optimization in python, including classical efficient frontier, Black-Litterman, Hierarchical Risk Parity

6,060 stars1,173 forksJupyter NotebookMIT

At a glance

What is it?
PyPortfolioOpt turns price data into portfolio weights through classical mean-variance optimization, Black-Litterman allocation and Hierarchical Risk Parity. It suits prototyping, not execution, and its own classifier still reads Beta.
Who is it for?
Adopt PyPortfolioOpt if you have a price table and want weights you can inspect, compare across optimizers and hand to a broker as share counts; the greedy_portfolio and lp_portfolio methods in pypfopt.discrete_allocation exist for exactly that last step. Do not adopt it if you need intraday rebalancing, live order routing or a service with an uptime commitment, because it is a library that runs when you call it and nothing more.
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 85 days ago.
What is it written in?
Mainly Jupyter Notebook, according to GitHub's language statistics.

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

Editorial analysis

What PyPortfolioOpt actually decides for you

The library takes two inputs, a vector of expected returns and a covariance matrix, and returns a vector of weights. Everything else in the package exists to produce better versions of those two inputs or to constrain what the optimizer is allowed to do with them. That split is the whole design. The README describes the project as inspired by scikit-learn, and the API follows that pattern: build an estimator object, call a fit-like method, read attributes off it.

The intended user is someone who already has a view. The README names two: a fundamentals-oriented investor with a handful of undervalued picks, and an algorithmic trader with a basket of strategies. In both cases the alpha comes from outside the library. PyPortfolioOpt answers a narrower question, which is how to size positions given that you think you know something. If you have no return forecast at all, the library will happily compute one from historical prices, and that is where most of the disappointment originates.

The data flow from price table to share counts

The canonical pipeline has four stages, and the README's quick example walks all of them. First, a pandas DataFrame of prices indexed by date. Second, expected_returns.mean_historical_return(df) for the return vector and risk_models.sample_cov(df) for the covariance matrix. Third, an EfficientFrontier object that solves for weights against an objective such as max_sharpe. Fourth, the discrete allocation layer, which converts continuous weights into whole shares given a total portfolio value.

The optimizer itself is built on cvxpy, which is a hard dependency in pyproject.toml alongside numpy, pandas, scikit-learn, scipy and scikit-base. That choice matters: constraints are expressed as convex expressions, so the library can accept position bounds, sector constraints and custom objectives without you writing a solver interface. The cost is that anything non-convex falls outside the supported set, and the failure mode is a solver error rather than a graceful fallback.

Between stages two and three sits the part that decides your results. mean_historical_return is a simple historical average. sample_cov is the sample covariance matrix. Neither is an estimate of the future, and on a short price history sample_cov is close to singular, which makes the inverse unstable and the weights extreme. The library ships alternatives for this reason, and the README lists shrinkage and Hierarchical Risk Parity among the implemented methods.

Installing PyPortfolioOpt and running a first optimization

The README gives pip as the primary route. The package name on PyPI is pyportfolioopt, and the import name is pypfopt, which is a mismatch worth remembering when you read error messages.

bash
pip install pyportfolioopt

Installing from source is the same command after a clone, which the README shows for anyone tracking main rather than a release.

bash
git clone https://github.com/PyPortfolio/PyPortfolioOpt.git
cd PyPortfolioOpt
pip install .

The README also documents an extras set, installed as pip install pyportfolioopt[all_extras], for the soft dependencies. matplotlib is listed inside that set, so plotting the efficient frontier requires the extras rather than the base install.

For a first real run, the README's example reads a CSV of prices, computes the two inputs and maximizes the Sharpe ratio. Copy the shape of it rather than the file path, since tests/resources/stock_prices.csv is a repository fixture, not something on your machine.

python
import pandas as pd
from pypfopt import EfficientFrontier
from pypfopt import risk_models
from pypfopt import expected_returns

df = pd.read_csv("prices.csv", parse_dates=True, index_col="date")
mu = expected_returns.mean_historical_return(df)
S = risk_models.sample_cov(df)

ef = EfficientFrontier(mu, S)
raw_weights = ef.max_sharpe()
cleaned_weights = ef.clean_weights()
ef.save_weights_to_file("weights.csv")

clean_weights rounds small positions down to zero, which is why the README's printed output shows several tickers at 0.0000. If you want the raw continuous solution instead, keep raw_weights. portfolio_performance(verbose=True) then reports expected annual return, annual volatility and the Sharpe ratio for whatever weights the object currently holds.

The last step converts weights into shares. get_latest_prices(df) pulls the final row of the price frame, and DiscreteAllocation takes the cleaned weights plus a total_portfolio_value.

python
from pypfopt.discrete_allocation import DiscreteAllocation, get_latest_prices

latest_prices = get_latest_prices(df)
da = DiscreteAllocation(cleaned_weights, latest_prices, total_portfolio_value=10000)
allocation, leftover = da.greedy_portfolio()
print(allocation)
print("Funds remaining: ${:.2f}".format(leftover))

The README's own run of this leaves $17.46 unspent out of $10,000. That residual is not a bug. Whole-share allocation is an integer problem and greedy_portfolio is a heuristic, so a leftover is expected. lp_portfolio is the other method on the same class if you want the integer program solved rather than approximated.

Where the mean-variance default breaks

The library's headline method is also its most fragile one. Mean-variance optimization is sensitive to the expected return vector, and mean_historical_return is a noisy estimate of it. Feed the optimizer noisy returns and an inverted covariance matrix and it responds the way the mathematics says it should: by concentrating in whichever assets happened to have the best recent sample mean and the lowest sample covariance. The README's example output puts roughly 33 percent in MA and 20 percent in PFE, which is a concentration that a historical average rarely justifies.

This is not a defect in the implementation. It is the reason the package ships shrinkage estimators, Black-Litterman and Hierarchical Risk Parity in the same distribution. The README presents all of them as options rather than ranking them, and the documentation does not tell you which to pick. That is a real gap for a newcomer: the quick example uses the approach most likely to produce unstable weights, and nothing in the README warns that the example is a demonstration rather than a recommendation.

The second limitation is scope. This is a library, not a system. There is no scheduler, no data feed, no order management, no persistence layer beyond save_weights_to_file. You supply the prices, you decide when to re-run, and you handle whatever happens between the weight file and the broker. For a monthly rebalance driven by a notebook, that is fine. For anything that needs to react to a market event, it is the wrong tool and no amount of configuration changes that.

Hierarchical Risk Parity against the mean-variance default

The README lists Hierarchical Risk Parity alongside classical mean-variance and Black-Litterman as one of the three method families. The difference in approach is worth stating plainly, because it changes what you have to supply. Mean-variance needs a return forecast, so you must produce mu from somewhere and defend it. HRP works from the covariance structure: it clusters assets by their correlation, then allocates down the resulting tree. No expected return vector goes in, so there is no forecast to get wrong.

That is the trade. You give up the ability to express a view in the optimizer, and you gain a portfolio that does not lurch when one asset's sample mean moves. For a fundamentals investor with a handful of picks, mean-variance plus a view is the more expressive tool. For someone holding a broad basket who mainly wants sensible risk weights, HRP removes an input that was doing more harm than good.

Black-Litterman sits in between and is the most demanding of the three. It takes a market-implied prior and blends it with your explicit views, which means you must supply both a prior and the views with their confidence levels. The README names it as a feature; the documentation is where the mechanics live, and the cookbook directory holds worked examples. If you are choosing between the three, the honest test is to run all of them on the same price frame and compare the weight vectors, not the reported Sharpe ratio, since that number is computed from the same historical estimates that produced the weights.

Release cadence, version pinning and the MIT licence

The gap between v1.4.1 in May 2021 and v1.6.0 in February 2026 is the relevant fact for anyone pinning a dependency. Five years passed between those two releases, so a project that depended on the 1.4 line and assumed steady minor updates would have been wrong. The last push to the repository was on 2026-07-07, which is recent, but the release history shows the maintainers do not ship on a schedule.

Version constraints deserve attention. pyproject.toml declares requires-python >=3.10,<3.15 and pins numpy >=1.26.0,<3.0.0, pandas >=1.0.0,<4.0.0, and scikit-base <0.14.0. The scikit-base upper bound is the tightest of these, and it is a transitive constraint you may not control if another package in your environment also depends on it. requirements.txt in the repository is looser than pyproject.toml, listing numpy>=1.0.0 and pandas>=0.19, which is a development file rather than the install contract. Trust pyproject.toml.

On licensing, the project is MIT, and the README points to the LICENSE file for the full text. MIT is permissive: it allows commercial use and modification, and it requires that the copyright notice and permission notice travel with copies. The README carries an explicit disclaimer that nothing in the project constitutes investment advice, and that disclaimer sits separately from the licence. Those are two different things, and the licence does not absorb the disclaimer. Whether either matters for your use is a question for your own counsel, not something this article can settle.

Editorial conclusion

Adopt PyPortfolioOpt if you have a price table and want weights you can inspect, compare across optimizers and hand to a broker as share counts; the greedy_portfolio and lp_portfolio methods in pypfopt.discrete_allocation exist for exactly that last step. Do not adopt it if you need intraday rebalancing, live order routing or a service with an uptime commitment, because it is a library that runs when you call it and nothing more. Before trusting any output, check three things: that requires-python >=3.10,<3.15 matches your interpreter, that your covariance matrix is not near-singular after you drop short price histories, and that clean_weights has zeroed the assets you expected rather than the ones you did not. The pyproject.toml classifier still says Development Status :: 4 - Beta, so treat the API as something that can move between minor releases.

Frequently asked questions

Can Python be used for portfolio optimization?

Yes. PyPortfolioOpt is a Python library that implements mean-variance optimization, Black-Litterman allocation and Hierarchical Risk Parity, and its README example goes from a price CSV to printed weights in a few lines. The heavy numerical work is delegated to numpy, pandas, scipy and cvxpy, which are declared as core dependencies.

What is the most effective portfolio optimization method?

The README does not rank its methods. It presents classical mean-variance optimization, Black-Litterman and Hierarchical Risk Parity as alternatives in the same package, and the quick example uses max_sharpe without claiming it is the best choice. Hierarchical Risk Parity avoids needing an expected return vector at all, which is the main structural difference between them.

What is Hierarchical Risk Parity (HRP)?

In PyPortfolioOpt, Hierarchical Risk Parity is one of the implemented allocation methods, listed in the README next to Black-Litterman and classical mean-variance optimization. It allocates from the covariance structure rather than from a return forecast, so it does not require the expected return vector that EfficientFrontier takes.

What is mean-variance optimization and how does it work?

It is the classical approach the README attributes to Harry Markowitz's 1952 paper, and it is what EfficientFrontier implements. You supply an expected return vector and a covariance matrix, and the optimizer solves for weights that maximize a risk-adjusted objective such as the Sharpe ratio.

Official sources

  1. License: MIT
  2. Project website
  3. PyPortfolio/PyPortfolioOpt 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/pyportfolio-pyportfolioopt.svg)](https://hysenlabs.com/projects/pyportfolio-pyportfolioopt)