Skip to main content

sperner

Split the rent of a shared flat so that nobody envies anybody, without anybody putting a price on a room. Each flatmate answers a few questions of the form "at these prices, which room would you take?". sperner then gives every room a price and every person a room they picked at those prices. It implements Francis Su's Rental Harmony (1999): a constructive proof of Sperner's lemma, turned into a questionnaire.

tests PyPI Python 3.10+ No dependencies License: MIT Open the app

  • Answers, not valuations. Nobody has to say what a room is worth in money. Budgets, a partner who stays over or a dislike of stairs enter the answers as they are.
  • About a dozen questions each for three flatmates, to within 10 on a rent of 3,000.
  • One flatmate still missing? With three rooms, two people can settle prices that work whichever room the third takes (Frick, Houston-Edwards and Meunier 2019).
  • Exact and checked. Pure Python without dependencies, integers and fractions in every decision, and every result says which answers it rests on.

Try it

pip install sperner
from sperner import split_rent
from sperner.people import QuasiLinear  # simulated flatmates for this example

rooms = ["Balcony", "Big", "Small"]
flatmates = {
    "Mia": QuasiLinear((1200, 900, 600)),
    "Jonas": QuasiLinear((1000, 1050, 650)),
    "Lea": QuasiLinear((1100, 800, 800)),
}


def ask(person, prices):
    # In real life: show `prices` (room -> rent) to `person`, return the room they pick.
    return rooms[flatmates[person].choose([float(prices[r]) for r in rooms])]


print(split_rent(rooms, 2400, list(flatmates), ask, tolerance=5))
Balcony  Mia              1,097.39
Big      Jonas              801.10
Small    Lea                501.51
Everyone picked their room at prices within 3.32 of these (28 questions: Mia 13, Jonas 6, Lea 9).

For an app, a form or a chat bot, RentSession asks one question at a time and can be saved as JSON between questions:

from sperner import RentSession

session = RentSession(rooms, 2400, ["Mia", "Jonas", "Lea"], tolerance=5)
while (question := session.next_question()) is not None:
    room = ...  # show question.prices to question.person
    session.answer(room)
print(session.result)

The web app at sperner.streamlit.app runs this on one phone that the flatmates pass around, so that nobody sees the others' answers. To run it yourself:

pip install -e ".[app]" && streamlit run streamlit_app.py

A flatmate who is not there yet

With three rooms and two names, split_rent returns prices and a plan for every room the newcomer might take:

print(split_rent(rooms, 2400, ["Mia", "Jonas"], ask, tolerance=5))
Balcony: 1,068.86
Big: 864.75
Small: 466.39
If the newcomer takes Balcony: Mia takes Small, Jonas takes Big.
If the newcomer takes Big: Mia takes Balcony, Jonas takes Small.
If the newcomer takes Small: Mia takes Balcony, Jonas takes Big.

How many questions?

Simulated flats with a rent of 3,000, 200 flats per row (benchmark, full results):

Flatmates Precision Questions per person, mean 90th percentile
2 10 3.9 5.0
3 30 10.1 14.0
3 10 12.3 17.0
3 3 14.4 20.7
4 10 19.2 28.8
5 10 38.7 62.8

Every split in these runs rested on answers alone, and the largest envy was 1.4 times the precision; the guarantee is twice the precision. Leaving a room for a newcomer costs more: 33 questions each for the two who answer, at precision 10.

Why answers and not valuations

Spliddit, the best-known rent calculator, asks everybody to value every room in money and assumes that a room's appeal is its value minus its price. That model cannot say "I can pay 1,100 at most". In the benchmark, three flatmates have budgets that together exceed the rent, and rent above a budget hurts four times as much:

Method Flats where someone pays over budget Envy under true preferences, mean Largest envy
Spliddit, given the values 66% 179.9 1,259.9
sperner, given answers 50% 1.3 13.2

In sperner's splits, a flatmate who pays more than their budget still prefers their room at its price to every other room at its price, within the precision. In Spliddit's they often do not. The cost is more input: about 12 questions each, against 3 numbers each.

Cakes and chores

divide is the general tool: n people and n pieces whose sizes add up to one. You supply pick(person, shares), which returns the index of the piece person would take if the pieces had sizes shares (fractions that add up to one).

from sperner import divide

division = divide(3, pick, tolerance=0.01)  # a cake: nobody takes an empty piece
division = divide(3, pick, bads=True, tolerance=0.01)  # chores: everybody takes an empty one
division.shares, division.assignment

How it works

Every division of the rent is a point of a simplex, a triangle for three rooms. sperner cuts it into small cells and gives the corners of every cell to different flatmates (Su 1999). The owner of a corner is asked which room they would take at the prices it stands for. A cell whose corners got all different rooms is an envy-free split, up to the size of a cell. Sperner's lemma (1928) guarantees such a cell, and the constructive proof of Cohen and Kuhn follows a path of neighbouring cells to one, asking only at the corners it meets. Nobody is asked about prices at which a room is free: a relabeling by Frick, Houston-Edwards and Meunier (2019) settles those without questions. Then the grid is refined around the cell found, three times finer each round.

docs/THEORY.md states the algorithms, their assumptions and what a result guarantees. The walk is available on its own, for any Sperner labeling:

from sperner import find_fully_labeled_cell

target = (0.2, 0.5, 0.3)


def label(point):  # Brouwer's labeling of the constant map to `target`
    size = sum(point)
    return next(i for i, t in enumerate(target) if point[i] > 0 and point[i] / size >= t)


walk = find_fully_labeled_cell(3, 1000, label)
walk.cell.points  # ((200, 499, 301), (199, 500, 301), (199, 499, 302))
walk.labeled  # 1405 of the grid's 501,501 points

Limitations

  • An assumption about free rooms. Without allow_negative, rents are between zero and the total, which assumes that everybody would take a free room over one they pay for. A result says which choices rest on that assumption rather than on answers (RentChoice.asked); in the benchmark, none did. For a room so bad that its tenant has to be paid, use allow_negative=True.
  • Approximate. Everybody picked their room at prices within precision of the final ones, not at the final prices themselves. A finer precision costs a few more questions.
  • Not strategy-proof. A flatmate who knows the others' answers can sometimes gain by lying. No envy-free rent division method is strategy-proof.
  • Many flatmates. Questions grow quickly with the number of people: five flatmates answer about 40 questions each.
  • The newcomer mode covers three rooms. Frick, Houston-Edwards and Meunier prove the case of n rooms too; it is not implemented yet.

Related tools

  • Spliddit computes exactly envy-free rents from values that everybody states in money (Gal, Mash, Procaccia and Zick 2017).
  • The New York Times' interactive rent calculator (2014) brought Su's method to a wide audience.
  • fairpy collects fair division algorithms in Python, among them rent division with hard budgets (Procaccia, Velez and Yu 2018).

Development

pip install -e ".[app,dev]"
pytest
ruff check .
python -m benchmarks.run --out benchmarks/results.md

See CONTRIBUTING.md.

Citation

If you use sperner in research, please cite it with the metadata in CITATION.cff, together with the papers it implements:

  • Su, F. E. (1999). Rental harmony: Sperner's lemma in fair division. American Mathematical Monthly 106(10), 930–942.
  • Frick, F., Houston-Edwards, K., Meunier, F. (2019). Achieving rental harmony with a secretive roommate. American Mathematical Monthly 126(1), 18–32.

Up to version 0.2, sperner was a multi-objective optimisation library; the changelog explains the change.

License

MIT

Metadata

Release files for sperner 0.3.0

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

Source distribution (sdist)

Source distribution for sperner 0.3.0
File Size Uploaded
sperner-0.3.0.tar.gz 44.9 kB Details

Built distribution (wheel)

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

Total release size: 67.6 kB

Release files / sperner-0.3.0.tar.gz

Download URL sperner-0.3.0.tar.gz
Size 44.9 kB
Tags Source
SHA-256 checksum
How to use checksums
3679bf116ff839f56bd2b3d0f6599434ae30f24936c45a6cad30a8f53e4c7b8f
BLAKE2b-256 checksum
How to use checksums
9c0c7eb4283a65583aa02bfce3c2ad89cde87efeb415adfb3c8f9351093135c2
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 Sep 11, 2026.

Transparency log

Release files / sperner-0.3.0-py3-none-any.whl

Download URL sperner-0.3.0-py3-none-any.whl
Size 22.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a62ec5e528b6670e0e6b59d6419ae1850467b90647e99c17ca04e0bdd7256cd9
BLAKE2b-256 checksum
How to use checksums
4d0a7d51fa3ced2215497b602d651e0b16bacb24cc6ca9d41877c29abeddce52
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 Sep 11, 2026.

Transparency log

Release history Release notifications | RSS feed

0.4.3

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

This release

0.3.0 This release

2 release files

0.2.0

2 release files

0.1.0

2 release files

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