Added
STVAnimationmodule (optionalmanimdependency) for visualizing STV elections (PR #249):ColorPalettedataclass for customizing candidate colors- Light/dark mode, user-specified fonts, and candidate nicknames
- Automatic focus on elected candidates; condensed reporting for offscreen events
- Inline rendering in interactive notebooks; explicit
.save()method for writing to file
SimultaneousVetoelection method with support for harmonic Borda scores (PR #333)SerialVetoelection class;PluralityVetoandSerialVetoare now subclasses of a sharedSequentialVetobase class (PR #325)- Schulze election method (PR #320, closes #318)
strictparameter toget_condorcet_cyclesfor detecting strict Condorcet cycles (PR #330, closes #327)FastIRVandFastSequentialRCVelection methods built onNumpyInnerSTVAlbanySTVelection method (closes #281)allow_zero_support_candidatesparameter toBlocSlateConfig, allowing candidates with zero support in preference intervals (PR #338, closes #298)- Missing docstrings for spatial ballot generator models (closes #328)
go-tasktask runner and updated contributing guide (PR #342, closes #277)
Changed
- Migrated package management to
uvand task running togo-task(PR #342, #352) - Replaced
black/isort/mypywithruff/tyfor formatting and type checking (PR #352) - Election class
__init__parameter names made more descriptive — see API Updates below (PR #355) - Major overhaul of
PluralityVeto(PR #325):tiebreak=Noneis no longer permitted; default is now'first_place'with'lex'as backup- New
tiebreak_orderattribute for inspecting the tiebreak order - ~50-60x faster for deterministic tiebreaks by using
profile.dfdirectly
BlocSlateConfigrefactored to reduce nesting; error and warning strings are now dynamically formatted (PR #338, closes #299)get_preference_interval_for_bloc_and_slatenow only validates the target bloc/slate rather than the full config (PR #338)- STV restructured as a subpackage;
NumpyElectionrefactored into the abstract base classNumpyInnerSTV - Removed
ElectionCore,STVCore, andNumpyElectionclasses PreferenceProfile.to_csvnow encodes candidate names with integer IDs (e.g.(Aleine:0),(Alex:1)) instead of the previous shortened prefix strings (e.g.(Aleine:Alei),(Alex:Alex)). This avoids ambiguity when candidates share long common prefixes. Old prefix-format CSVs are still fully readable byfrom_csv(PR #361)BlockPluralitymoved out of theapprovalsubmodule and now accepts bothRankProfileandScoreProfileinputs, dispatching to the appropriate ranked or score-based implementation (PR #360, closes #351)RankingElectionbase class now validates that the profile is non-empty, contains at least one ranked candidate, and has enough candidates who received votes to fill the requested seats (PR #360, closes #356)
Fixed
- Fixed
index_to_lexicographic_ballotto correctly handle short ballots (PR #348) - Fixed scoring functions for profiles where a ballot ranked more candidates than
max_ranking_lengthvia ties (PR #334) - Fixed a deletion desync in
BlocSlateConfig(PR #338) - Fixed
PluralityVetoto treat unranked candidates as tied for last place and thus eligible to be vetoed (PR #325) - Fixed
pairwise_dictto usecandidates_castinstead ofcandidates, preventing errors in elections likeRankedPairswhen a candidate received no votes (PR #361, closes #309) - Fixed flaky
BoostedRandomDictatorandRandomDictatortests (PR #361, closes #339) - Fixed broken link in readthedocs (PR #360, closes #350)
- Pre-commit hooks now only run on staged files (PR #360)
- Tests now run in parallel via
pytest-xdist(-n auto). Bijection tests write totmp_path
instead of fixed data directories, and random seed usage is properly isolated.
API Updates
Deprecated: m renamed to n_seats across all election classes (PR #355)
The m keyword argument has been renamed to n_seats in every election class.
Using m still works but emits a DeprecationWarning and will be removed in a future version.
Affected classes: Approval, BlockPlurality, Borda, CondoBorda, SNTV, Plurality,
Alaska, BoostedRandomDictator, RandomDictator, RankedPairs, PluralityVeto,
SerialVeto, SimultaneousVeto, Schulze, STV, IRV, SequentialRCV, FastSTV,
AlbanySTV, FastIRV, FastSequentialRCV, GeneralRating, Rating, Limited, Cumulative
Also affects r_representation_score(m, ...) → r_representation_score(n_seats, ...).
# Deprecated (still works, emits warning)
STV(profile, m=3)
# Recommended
STV(profile, n_seats=3)Deprecated: GeneralRating parameter renames (PR #355)
Old names still accepted with a DeprecationWarning.
| Old parameter | New parameter | Notes |
|---|---|---|
m |
n_seats |
Number of seats |
k |
per_candidate_limit |
Per-candidate score cap |
| (new) | budget |
Total per-voter budget (was unnamed) |
Deprecated: Cumulative and Limited parameter rename (PR #355)
Old names still accepted with a DeprecationWarning.
| Old parameter | New parameter |
|---|---|
m |
n_seats |
k |
budget |
Breaking: BlocPlurality renamed to BlockPlurality (PR #355)
BlocPlurality still works but now raises a DeprecationWarning. Use BlockPlurality instead.
# Deprecated
from votekit.elections import BlocPlurality
# Use instead
from votekit.elections import BlockPluralityNew election classes
SimultaneousVeto (votekit.elections)
SimultaneousVeto(
profile: RankProfile,
n_seats: int = 1,
candidate_weights: Literal["first_place", "uniform", "borda", "harmonic"]
| dict[str, float] | int = "first_place",
tiebreak: Optional[str] = None,
scoring_tie_convention: Literal["average", "high", "low"] = "low",
allow_bullet_veto: bool = False,
)SerialVeto (votekit.elections) — variant of PluralityVeto where a candidate with zero
score is only eliminated when a voter attempts to veto them (rather than being immediately removed).
Schulze (votekit.elections, closes #318)
Schulze(profile: RankProfile, n_seats: int = 1)Changed: PluralityVeto signature and behavior (PR #325)
PluralityVeto(
profile: RankProfile,
n_seats: int = 1,
tiebreak: Literal["first_place", "borda", "random", "lex"] = "first_place",
)tiebreak=Noneis no longer accepted;'first_place'is the default with'lex'as automatic backup
when first-place votes are tied.- New attribute
tiebreak_order: Optional[tuple[frozenset[str]]]— the pre-computed ordering used to
resolve last-place ties. IsNonewhentiebreak='random'. - Unranked candidates are now treated as tied for last place and are eligible to be vetoed (bug fix).
- ~50–60x faster for deterministic tiebreaks on large profiles.
Changed: get_condorcet_cycles new strict parameter (PR #330, closes #327)
PairwiseComparisonGraph.get_condorcet_cycles(strict: bool = False) -> list[list[str]]When strict=True, only strict wins (edge weight > 0) are considered; ties are excluded.
Defaults to False for backward compatibility.
New: BlocSlateConfig.allow_zero_support_candidates (PR #338, closes #298)
BlocSlateConfig(
...,
allow_zero_support_candidates: bool = False,
)When True, candidates may have zero support in a PreferenceInterval without raising an error.
Defaults to False, preserving existing behavior.
Changed: BlockPlurality now supports ranked profiles (PR #360, closes #351)
BlockPlurality has been moved out of the approval submodule. It now accepts either a
RankProfile or a ScoreProfile and dispatches to the appropriate implementation.
# Ranked profile — top `budget` candidates each receive 1 point
BlockPlurality(profile: RankProfile, n_seats=1, budget=None, tiebreak=None)
# Score profile — voters give at most 1 point to each of `budget` candidates
BlockPlurality(profile: ScoreProfile, n_seats=1, budget=None, tiebreak=None)Changed: PreferenceProfile.to_csv candidate encoding and in-memory support (PR #361)
CSV files now use integer IDs for candidate labels instead of shortened name prefixes.
Old-format CSVs (with prefix labels) are still readable by from_csv.
# Old format
(Aleine:Alei),(Alex:Alex),(C:C)
# New format
(Aleine:0),(Alex:1),(C:2)
to_csv now defaults fpath to None. When no path is given, it returns the CSV content
as a string instead of writing to disk. This is useful for writing profiles into zip files
or other in-memory workflows without intermediate files.
# Write to disk (unchanged)
profile.to_csv("output.csv")
# Get CSV as a string
csv_str = profile.to_csv()
# Write multiple profiles into a zip
import zipfile, io
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as zf:
for name, profile in profiles.items():
zf.writestr(f"{name}.csv", profile.to_csv())New: STVAnimation module (PR #249)
Requires the optional manim dependency (pip install votekit[manim]).
from votekit.animations import STVAnimation, ColorPalette, DARK_PALETTE, LIGHT_PALETTE
anim = STVAnimation(
election: STV,
title: Optional[str] = None,
focus: set[str] | list[str] | Literal["winners", "viable", "all"] = "viable",
nicknames: Optional[dict[str, str]] = None,
candidate_colors: Optional[Mapping[str, ParsableManimColor]] = None,
color_palette: ColorPalette = DARK_PALETTE,
font: str = "",
)
anim.render()
anim.save(save_path) # renders first if not already renderedNew Contributors
- @jeqcho made their first contribution in #320
- @hayan-gh made their first contribution in #329
- @ross-i made their first contribution in #334
- @prismika made their first contribution in #249
Full Changelog: v3.3.1...v3.4.0