Expected Parrot · a practical, evidence-first tutorial

Counting real ballots under twelve voting methods

Eighteen people each ranked eight classic novels. This tutorial imports their ballots as a dataset and shows how different ways of tallying them do (or don't) change the outcome.

01 The question and the data

Different voting methods can elect different winners from the same ballots — that is the entire reason this tool exists. Plurality rewards first-place strength; Borda rewards broad support; Condorcet methods reward head-to-head dominance; IRV rewards surviving eliminations. voting stores one set of inputs — options, voters, ballots — and lets any of 32 method names and aliases count them, so the comparison is always apples to apples.

To be clear about what this is: voting is a research instrument — for studying group preferences, comparing counting rules, teaching social choice, and settling low-stakes group decisions (which book, which logo, which restaurant). The vocabulary is the social-choice literature's — elections, voters, ballots — but nothing here administers civic elections, and nothing should be read as claiming to.

The worked example uses real data: eighteen people each ranked the eight novels, and their full rankings live in a production EDSL Results object that this page imports — actual human preferences, not synthetic data. (How you collect such a dataset — hosted surveys, email invitations, AI personas — is its own topic: voting docs show humanize and voting docs show recipes.)

If you follow along, expect small differences. Every command output on this page is a real captured envelope from one worked run. Ballot and result ids are generated per run, and timestamps will differ. The one step you cannot reproduce byte-for-byte is the data itself: the production Results object belongs to its owner's account, so substitute your own survey results (or cast ballots directly with voting ballot rank). Everything else — every command and the shape of every output — is what you will see. Outputs are JSON envelopes by default; add --human for a readable rendering.

02 Installation and the output contract

uv tool install git+https://github.com/expectedparrot/voting.git

(uv tool gives voting its own isolated environment on your PATH; plain pip install works too.) Confirm the install:

voting version
Show command output
{
  "schema_version": "1.0",
  "command": "version",
  "status": "ok",
  "argv": [
    "voting",
    "version"
  ],
  "data": {
    "version": "0.1.0",
    "package_path": "~/voting/voting",
    "envelope_schema_version": "1.0"
  },
  "warnings": [],
  "errors": [],
  "next_steps": []
}

That envelope is the whole interface. Every command prints exactly one JSON envelope to stdout — schema_version, command, status, argv, data, warnings, errors, next_steps — with failures as structured errors and nonzero exits. Add --human to any command for tables meant for people instead. voting capabilities states the contract machine-readably, including which commands touch the outside world:

voting capabilities
Show command output
{
  "schema_version": "1.0",
  "command": "capabilities",
  "status": "ok",
  "argv": [
    "voting",
    "capabilities"
  ],
  "data": {
    "envelope_schema_version": "1.0",
    "output_contract": "Every command prints one JSON envelope {schema_version, command, status, argv, data, warnings, errors, next_steps} to stdout by default; --human is an opt-in presentation mode.",
    "execution_boundary": "voting never executes model calls. `survey generate` builds a .jobs.ep package the user executes externally with `ep run`; counting and analysis are local.",
    "external_service_actions": {
      "survey publish": "Creates a hosted Humanize survey via the ep CLI (outward-facing).",
      "survey email": "Sends invitation emails to configured voters via the ep CLI (outward-facing).",
      "survey responses": "Downloads Humanize responses via the ep CLI (network read)."
    },
    "workflow_commands": [
      "voting status",
      "voting next",
      "voting docs list"
    ]
  },
  "warnings": [],
  "errors": [],
  "next_steps": []
}
Longer envelopes on this page are elided — lists cut to a few entries, repeated warnings summarized — so the shape stays readable. The first few outputs are shown complete; the full captures live in build/tutorial/captures/ when you regenerate the page.

03 Set up the project

Create a project and step into it. All state lives inside the project directory as plain JSON files, so everything the tool does is inspectable and versionable — the layout is covered at the end.

voting init classic_books
Show command output
{
  "schema_version": "1.0",
  "command": "init",
  "status": "ok",
  "argv": [
    "voting",
    "init",
    "classic_books"
  ],
  "data": {
    "project": ".",
    "data_dir": ".voting",
    "meta": {
      "id": "classic_books",
      "title": "Classic Books",
      "description": "",
      "created_at": "2026-07-28T14:34:15",
      "settings": {
        "default_tie_policy": "lexicographic",
        "allow_unregistered_voters": false
      }
    }
  },
  "warnings": [],
  "errors": [],
  "next_steps": [
    "voting option add <id> <name>",
    "voting voter add <id> <name>",
    "voting election add <id> <name>"
  ]
}
cd classic_books

voting next is the orientation command: it reports the current phase and the exact commands that make sense now. It exists mostly for AI agents driving the CLI — an agent that only knows how to run voting next can navigate the whole workflow — but it is just as useful when a person comes back to a project cold:

voting next
Show command output
{
  "schema_version": "1.0",
  "command": "next",
  "status": "ok",
  "argv": [
    "voting",
    "next"
  ],
  "data": {
    "phase": "setup",
    "counts": {
      "options": 0,
      "voters": 0,
      "elections": 0,
      "ballots": 0,
      "results": 0
    },
    "recommendation": "voting option add <id> <name>",
    "reason": "Add first option",
    "checklist": [
      "Add options (candidates/proposals): `voting option add <id> <name>`",
      "Add voters: `voting voter add <id> <name>`",
      "Optionally set voter traits for survey generation: `voting voter set-trait <id> <key> <value>`"
    ]
  },
  "warnings": [],
  "errors": [],
  "next_steps": [
    "voting option add <id> <name>",
    "voting voter add <id> <name>"
  ]
}

04 Define the election

An election is the central object here: it names the contest, fixes the ballot type (ranked, in this case — voters order all options; other types are single choice, approval, and score), and holds the list of eligible options. Deliberately absent: a counting method. The ballot type determines what voters are asked; how the ballots are counted is a lens you apply afterwards — as many lenses as you like, which is the whole point of chapter 6. An election starts as a draft and only accepts ballots once opened:

voting election add book_preference 'Classic book preference' --ballot-type ranked
Show command output
{
  "schema_version": "1.0",
  "command": "election add",
  "status": "ok",
  "argv": [
    "voting",
    "election",
    "add",
    "book_preference",
    "Classic book preference",
    "--ballot-type",
    "ranked"
  ],
  "data": {
    "id": "book_preference",
    "name": "Classic book preference",
    "description": "",
    "created_at": "2026-07-28T14:34:15",
    "ballot_type": "ranked",
    "seats": 1,
    "status": "draft",
    "options": [],
    "settings": {
      "tie_policy": "lexicographic",
      "quota": "droop"
    }
  },
  "warnings": [],
  "errors": [],
  "next_steps": [
    "voting election add-option book_preference <option_id>",
    "voting election open book_preference"
  ]
}

The eight books load in one step from a JSON file — id plus display name each. The display names matter: when ballots arrive from a survey, answers reference options by these exact labels, and the importer maps labels back to ids. An unknown label skips that row and reports it; nothing is ever silently guessed. (voting option add exists for adding one at a time.)

Alongside options, a project also keeps a voter roster — who is in your panel, at what weight, with what traits (personas for AI studies, emails for invitations). Ours starts empty — the respondents in the imported dataset join it automatically in chapter 5.

cat books.json
Show books.json
[
  {
    "id": "great_gatsby",
    "name": "The Great Gatsby \u2014 Fitzgerald"
  },
  {
    "id": "mockingbird",
    "name": "To Kill a Mockingbird \u2014 Harper Lee"
  },
  {
    "id": "nineteen_eighty_four",
    "name": "1984 \u2014 Orwell"
  },
  {
    "id": "catcher_rye",
    "name": "The Catcher in the Rye \u2014 Salinger"
  },
  {
    "id": "lord_flies",
    "name": "Lord of the Flies \u2014 Golding"
  },
  {
    "id": "mice_men",
    "name": "Of Mice and Men \u2014 Steinbeck"
  },
  {
    "id": "animal_farm",
    "name": "Animal Farm \u2014 Orwell"
  },
  {
    "id": "scarlet_letter",
    "name": "The Scarlet Letter \u2014 Hawthorne"
  }
]
voting option import --from books.json --election book_preference
Show command output
{
  "schema_version": "1.0",
  "command": "option import",
  "status": "ok",
  "argv": [
    "voting",
    "option",
    "import",
    "--from",
    "books.json",
    "--election",
    "book_preference"
  ],
  "data": {
    "imported": 8,
    "options": [
      {
        "id": "great_gatsby",
        "name": "The Great Gatsby — Fitzgerald"
      },
      {
        "id": "mockingbird",
        "name": "To Kill a Mockingbird — Harper Lee"
      },
      {
        "id": "nineteen_eighty_four",
        "name": "1984 — Orwell"
      },
      "… 5 further entries elided …"
    ],
    "election_id": "book_preference",
    "attached": [
      "great_gatsby",
      "mockingbird",
      "nineteen_eighty_four",
      "… 5 further entries elided …"
    ]
  },
  "warnings": [],
  "errors": [],
  "next_steps": [
    "voting election open book_preference",
    "voting --human election show book_preference"
  ]
}

Open the election and look at it the way a person would — --human renders tables instead of JSON:

voting election open book_preference
Show command output
{
  "schema_version": "1.0",
  "command": "election open",
  "status": "ok",
  "argv": [
    "voting",
    "election",
    "open",
    "book_preference"
  ],
  "data": {
    "ballot_type": "ranked",
    "created_at": "2026-07-28T14:34:15",
    "description": "",
    "id": "book_preference",
    "name": "Classic book preference",
    "options": [
      "great_gatsby",
      "mockingbird",
      "nineteen_eighty_four",
      "catcher_rye",
      "lord_flies",
      "mice_men",
      "animal_farm",
      "scarlet_letter"
    ],
    "seats": 1,
    "settings": {
      "quota": "droop",
      "tie_policy": "lexicographic"
    },
    "status": "open",
    "open_at": "2026-07-28T14:34:15"
  },
  "warnings": [],
  "errors": [],
  "next_steps": [
    "voting ballot rank book_preference <voter_id> <opt1> <opt2> ...",
    "voting survey generate book_preference"
  ]
}
voting --human election show book_preference
NameClassic book preference
Ballot typeranked
Statusopen
Seats1
Options (8)The Great Gatsby — Fitzgerald, To Kill a Mockingbird — Harper Lee, 1984 — Orwell, The Catcher in the Rye — Salinger, Lord of the Flies — Golding, Of Mice and Men — Steinbeck, Animal Farm — Orwell, The Scarlet Letter — Hawthorne
The election at a glance: eight options, ranked ballots, open for ballots — and no counting method, because that decision belongs to the count. (In a terminal, --human draws this same view; the page renders the captured envelope natively.)

05 Import real ballots

The eighteen rankings are a dataset: an EDSL Results object on the Expected Parrot platform. ballot import reads one directly — from a local .ep package (--from-results) or by UUID (--from-coop, a network read using your EDSL credentials) — and converts each response into a ballot: display labels map back to option ids (an unknown label is itemized, never guessed), and --register-voters adds each respondent to the project's voter roster at weight 1.0, keeping the original respondent id as provenance:

voting ballot import --election book_preference \
  --from-coop 917f9e19-d477-43cf-bdf0-664a1592400f --register-voters
Show command output
{
  "schema_version": "1.0",
  "command": "ballot import",
  "status": "ok",
  "argv": [
    "voting",
    "ballot",
    "import",
    "--election",
    "book_preference",
    "--from-coop",
    "917f9e19-d477-43cf-bdf0-664a1592400f",
    "--register-voters"
  ],
  "data": {
    "election_id": "book_preference",
    "cast": 18,
    "skipped": 0,
    "skipped_detail": [],
    "source_file": "coop:917f9e19-d477-43cf-bdf0-664a1592400f"
  },
  "warnings": [
    {
      "code": "voter_registered",
      "voter_id": "r_95fa122e_8606_4b48_8903_a342103fedb3"
    },
    {
      "code": "voter_registered",
      "voter_id": "r_02a2ae52_9765_414d_9d2e_9cfc19942690"
    },
    "… 16 further warnings elided (18 total) …"
  ],
  "errors": [],
  "next_steps": [
    "voting count run book_preference --method <method>"
  ]
}
Following along without a survey? This is the one step you cannot reproduce with this page's data (the Results object belongs to its owner). Cast a few ballots yourself instead and everything downstream works the same:
voting voter add v1 'Voter One'
voting ballot rank book_preference v1 nineteen_eighty_four mockingbird great_gatsby
Or run your own survey (voting survey publish) — or have AI personas vote: voting survey generate builds a job you execute with ep run and import with --from-results.

One habit worth keeping: validate before counting. valid_ballots is the number of ballots that will actually enter a tally — anything else (a respondent not on the roster, an unknown option, a duplicate ranking) is itemized as a warning and left out rather than guessed at. (Without --register-voters, for instance, all eighteen ballots would have been recorded but none would count until you decided whose responses belong in the analysis.)

voting ballot validate book_preference
Show command output
{
  "schema_version": "1.0",
  "command": "ballot validate",
  "status": "ok",
  "argv": [
    "voting",
    "ballot",
    "validate",
    "book_preference"
  ],
  "data": {
    "valid_ballots": 18,
    "warnings": []
  },
  "warnings": [],
  "errors": [],
  "next_steps": [
    "voting count run book_preference --method <method>"
  ]
}

Eighteen countable ballots. Here is the dataset — one row per respondent, top choices first:

voting --human ballot list --election book_preference
voterballot (top choices)recorded
r_95fa122e_86…nineteen_eighty_four > lord_flies > scarlet_letter … (+5 more)2026-07-28 14:34:17
r_02a2ae52_97…catcher_rye > nineteen_eighty_four > great_gatsby … (+5 more)2026-07-28 14:34:17
r_6785ca96_b4…nineteen_eighty_four > animal_farm > great_gatsby … (+5 more)2026-07-28 14:34:17
r_253c40f1_32…nineteen_eighty_four > mice_men > mockingbird … (+5 more)2026-07-28 14:34:17
r_2fbe4384_33…mice_men > great_gatsby > lord_flies … (+5 more)2026-07-28 14:34:17
r_0f85628b_50…nineteen_eighty_four > scarlet_letter > catcher_rye … (+5 more)2026-07-28 14:34:17
b29fe300_0612…nineteen_eighty_four > animal_farm > mice_men … (+5 more)2026-07-28 14:34:17
r_60abb9b9_1c…nineteen_eighty_four > animal_farm > catcher_rye … (+5 more)2026-07-28 14:34:17
r_4e8ddeca_81…mockingbird > mice_men > nineteen_eighty_four … (+5 more)2026-07-28 14:34:17
r_51e15daa_72…mockingbird > nineteen_eighty_four > lord_flies … (+5 more)2026-07-28 14:34:17
b2d6c619_8c5e…nineteen_eighty_four > great_gatsby > mockingbird … (+5 more)2026-07-28 14:34:17
r_2dcf9550_88…catcher_rye > great_gatsby > mice_men … (+5 more)2026-07-28 14:34:17
r_866b5ae1_22…mockingbird > great_gatsby > scarlet_letter … (+5 more)2026-07-28 14:34:17
abb414e9_cd1e…nineteen_eighty_four > animal_farm > lord_flies … (+5 more)2026-07-28 14:34:17
r_7cb11170_5a…mockingbird > nineteen_eighty_four > mice_men … (+5 more)2026-07-28 14:34:17
r_5b8d9c86_19…great_gatsby > scarlet_letter > mockingbird … (+5 more)2026-07-28 14:34:17
r_7df8a708_8e…great_gatsby > nineteen_eighty_four > lord_flies … (+5 more)2026-07-28 14:34:17
cd27fba8_b7ee…animal_farm > catcher_rye > great_gatsby … (+5 more)2026-07-28 14:34:17
The imported dataset: eighteen anonymous respondents at weight 1.0, top three choices shown; full rankings live in the ballot records and drive every count.

Eighteen full rankings are hard to eyeball. The built-in plots turn them into a picture — voting plot ranks shows, for each book, how many voters placed it first, second, and so on (hover any segment for the exact count):

voting plot ranks book_preference
Show command output
{
  "schema_version": "1.0",
  "command": "plot ranks",
  "status": "ok",
  "argv": [
    "voting",
    "plot",
    "ranks",
    "book_preference"
  ],
  "data": {
    "election_id": "book_preference",
    "ballots": 18,
    "path": ".voting/output/plots/ranks_book_preference.svg",
    "format": "svg"
  },
  "warnings": [],
  "errors": [],
  "next_steps": [
    "open .voting/output/plots/ranks_book_preference.svg"
  ]
}
Where voters ranked each option Each bar spans all 18 ballots; darker = ranked closer to first choice. 1984 — Orwell 1984 — Orwell: ranked #1 by 8 8 1984 — Orwell: ranked #2 by 4 1984 — Orwell: ranked #3 by 1 1984 — Orwell: ranked #4 by 1 1984 — Orwell: ranked #7 by 4 To Kill a Mockingbird — Har… To Kill a Mockingbird — Harper Lee: ranked #1 by 4 4 To Kill a Mockingbird — Harper Lee: ranked #3 by 3 To Kill a Mockingbird — Harper Lee: ranked #4 by 6 To Kill a Mockingbird — Harper Lee: ranked #5 by 3 To Kill a Mockingbird — Harper Lee: ranked #6 by 1 To Kill a Mockingbird — Harper Lee: ranked #7 by 1 The Great Gatsby — Fitzgera… The Great Gatsby — Fitzgerald: ranked #1 by 2 2 The Great Gatsby — Fitzgerald: ranked #2 by 4 The Great Gatsby — Fitzgerald: ranked #3 by 3 The Great Gatsby — Fitzgerald: ranked #4 by 1 The Great Gatsby — Fitzgerald: ranked #5 by 3 The Great Gatsby — Fitzgerald: ranked #6 by 3 The Great Gatsby — Fitzgerald: ranked #7 by 1 The Great Gatsby — Fitzgerald: ranked #8 by 1 Of Mice and Men — Steinbeck Of Mice and Men — Steinbeck: ranked #1 by 1 1 Of Mice and Men — Steinbeck: ranked #2 by 2 Of Mice and Men — Steinbeck: ranked #3 by 3 Of Mice and Men — Steinbeck: ranked #4 by 3 Of Mice and Men — Steinbeck: ranked #5 by 5 Of Mice and Men — Steinbeck: ranked #6 by 1 Of Mice and Men — Steinbeck: ranked #7 by 2 Of Mice and Men — Steinbeck: ranked #8 by 1 Animal Farm — Orwell Animal Farm — Orwell: ranked #1 by 1 1 Animal Farm — Orwell: ranked #2 by 4 Animal Farm — Orwell: ranked #4 by 4 Animal Farm — Orwell: ranked #5 by 2 Animal Farm — Orwell: ranked #6 by 4 Animal Farm — Orwell: ranked #7 by 1 Animal Farm — Orwell: ranked #8 by 2 The Catcher in the Rye — Sa… The Catcher in the Rye — Salinger: ranked #1 by 2 2 The Catcher in the Rye — Salinger: ranked #2 by 1 The Catcher in the Rye — Salinger: ranked #3 by 2 The Catcher in the Rye — Salinger: ranked #4 by 2 The Catcher in the Rye — Salinger: ranked #5 by 1 The Catcher in the Rye — Salinger: ranked #6 by 3 The Catcher in the Rye — Salinger: ranked #7 by 4 The Catcher in the Rye — Salinger: ranked #8 by 3 Lord of the Flies — Golding Lord of the Flies — Golding: ranked #2 by 1 Lord of the Flies — Golding: ranked #3 by 4 Lord of the Flies — Golding: ranked #4 by 1 Lord of the Flies — Golding: ranked #5 by 2 Lord of the Flies — Golding: ranked #6 by 1 Lord of the Flies — Golding: ranked #7 by 3 Lord of the Flies — Golding: ranked #8 by 6 The Scarlet Letter — Hawtho… The Scarlet Letter — Hawthorne: ranked #2 by 2 The Scarlet Letter — Hawthorne: ranked #3 by 2 The Scarlet Letter — Hawthorne: ranked #5 by 2 The Scarlet Letter — Hawthorne: ranked #6 by 5 The Scarlet Letter — Hawthorne: ranked #7 by 2 The Scarlet Letter — Hawthorne: ranked #8 by 5
Position distributions from the real ballots. nineteen_eighty_four's long dark leading edge is its 8 first-place votes; books lower down live mostly in the pale right-hand (late-rank) end of the bar.

voting status is the project's dashboard — 8 options, 18 voters, 18 ballots, and a phase saying counting is what remains:

voting status
Show command output
{
  "schema_version": "1.0",
  "command": "status",
  "status": "ok",
  "argv": [
    "voting",
    "status"
  ],
  "data": {
    "phase": "counting",
    "project_exists": true,
    "counts": {
      "options": 8,
      "voters": 18,
      "elections": 1,
      "ballots": 18,
      "results": 0
    },
    "checklist": [
      "Run a counting method: `voting count run <election_id> --method <method>`",
      "Run multiple methods for comparison: `voting count run <election_id> --method <method>`"
    ],
    "recommended_next_steps": [
      {
        "label": "Count the ballots",
        "command": "voting count run <election_id> --method <method>"
      }
    ]
  },
  "warnings": [],
  "errors": [],
  "next_steps": [
    "voting count run <election_id> --method <method>"
  ]
}

06 Every method, one answer

Now the point of the exercise. Counts name their method explicitly (voting count run <id> --method borda) — there is no default, and that is a feature: a "winner" is always a method's winner. But nobody should have to type twelve commands to ask the obvious question. voting count compare counts the same ballots under every method that can read them — for ranked ballots, all 12 — and saves each result:

voting count compare book_preference
Show command output
{
  "schema_version": "1.0",
  "command": "count compare",
  "status": "ok",
  "argv": [
    "voting",
    "count",
    "compare",
    "book_preference"
  ],
  "data": {
    "election_id": "book_preference",
    "ballot_type": "ranked",
    "methods_run": [
      "borda",
      "irv",
      "stv",
      "… 9 further entries elided …"
    ],
    "results": [
      {
        "method": "borda",
        "result_id": "20260728T183418670409Z_f264c771_book_preference_borda",
        "winners": [
          "nineteen_eighty_four"
        ],
        "runner_up": "mockingbird"
      },
      {
        "method": "irv",
        "result_id": "20260728T183418670959Z_9235f748_book_preference_irv",
        "winners": [
          "nineteen_eighty_four"
        ],
        "runner_up": "mockingbird"
      },
      {
        "method": "stv",
        "result_id": "20260728T183418671304Z_3c6e721b_book_preference_stv",
        "winners": [
          "nineteen_eighty_four"
        ],
        "runner_up": "mockingbird"
      },
      "… 9 further entries elided …"
    ],
    "no_winner": [
      "simple_majority"
    ],
    "unanimous_winners": [
      "nineteen_eighty_four"
    ],
    "summary": {
      "valid_ballots": 18,
      "invalid_ballots": 0
    }
  },
  "warnings": [],
  "errors": [],
  "next_steps": [
    "voting plot methods --election book_preference",
    "voting count show 20260728T183418670409Z_f264c771_book_preference_borda"
  ]
}
voting --human count list --election book_preference
methodwinnerrunner-up
bordanineteen_eighty_fourmockingbird
irvnineteen_eighty_fourmockingbird
stvnineteen_eighty_fourmockingbird
schulzenineteen_eighty_fourgreat_gatsby
copelandnineteen_eighty_fourgreat_gatsby
minimaxnineteen_eighty_fourmockingbird
ranked_pairsnineteen_eighty_fourgreat_gatsby
kemeny_youngnineteen_eighty_fourgreat_gatsby
bucklinnineteen_eighty_fourgreat_gatsby
runoffnineteen_eighty_fourmockingbird
fptpnineteen_eighty_fourmockingbird
simple_majority(no winner)mockingbird
Twelve methods, side by side. 11 elect nineteen_eighty_four; simple_majority declines.

11 of 12 methods elect 1984. The twelfth refuses to answer — and its refusal is worth reading. simple_majority requires an outright majority of first preferences; nineteen_eighty_four holds 8 of 18, a plurality but not a majority, so the method reports winners: [] with the threshold it applied. Like the valid_ballots check in chapter 5, an honest refusal beats a fabricated answer.

Every comparison run is a full saved record of how its method reasoned. Open a few with count show. Borda first — each ballot position contributes points, so it measures breadth of support:

voting count show 20260728T183418670409Z_f264c771_book_preference_borda
Show command output
{
  "schema_version": "1.0",
  "command": "count show",
  "status": "ok",
  "argv": [
    "voting",
    "count",
    "show",
    "20260728T183418670409Z_f264c771_book_preference_borda"
  ],
  "data": {
    "created_at": "2026-07-28T14:34:18",
    "election_id": "book_preference",
    "id": "20260728T183418670409Z_f264c771_book_preference_borda",
    "method": "borda",
    "ranking": [
      {
        "option_id": "nineteen_eighty_four",
        "rank": 1,
        "score": 93.0,
        "status": "elected"
      },
      {
        "option_id": "mockingbird",
        "rank": 2,
        "score": 79.0,
        "status": "defeated"
      },
      {
        "option_id": "great_gatsby",
        "rank": 3,
        "score": 73.0,
        "status": "defeated"
      },
      "… 5 further entries elided …"
    ],
    "rounds": [],
    "scores": [
      {
        "option_id": "nineteen_eighty_four",
        "total": 93.0
      },
      {
        "option_id": "mockingbird",
        "total": 79.0
      },
      {
        "option_id": "great_gatsby",
        "total": 73.0
      },
      "… 5 further entries elided …"
    ],
    "settings": {
      "quota": "droop",
      "seats": 1,
      "tie_policy": "lexicographic"
    },
    "summary": {
      "exhausted_weight": 0.0,
      "invalid_ballots": 0,
      "total_valid_weight": 18.0,
      "valid_ballots": 18
    },
    "warnings": [],
    "winners": [
      "nineteen_eighty_four"
    ]
  },
  "warnings": [],
  "errors": [],
  "next_steps": []
}
voting --human count show 20260728T183418670409Z_f264c771_book_preference_borda
rankoptionscorestatus
1nineteen_eighty_four93elected
2mockingbird79defeated
3great_gatsby73defeated
4mice_men65defeated
5animal_farm62defeated
6catcher_rye51defeated
7lord_flies41defeated
8scarlet_letter40defeated
The saved Borda count: full ranking, scores, and the winner.
voting plot scores 20260728T183418670409Z_f264c771_book_preference_borda
Show command output
{
  "schema_version": "1.0",
  "command": "plot scores",
  "status": "ok",
  "argv": [
    "voting",
    "plot",
    "scores",
    "20260728T183418670409Z_f264c771_book_preference_borda"
  ],
  "data": {
    "result_id": "20260728T183418670409Z_f264c771_book_preference_borda",
    "method": "borda",
    "path": ".voting/output/plots/scores_20260728T183418670409Z_f264c771_book_preference_borda.svg",
    "format": "svg"
  },
  "warnings": [],
  "errors": [],
  "next_steps": [
    "open .voting/output/plots/scores_20260728T183418670409Z_f264c771_book_preference_borda.svg"
  ]
}
borda totals — book_preference 1984 — Orwell 93 To Kill a Mockingbird — Har… 79 The Great Gatsby — Fitzgera… 73 Of Mice and Men — Steinbeck 65 Animal Farm — Orwell 62 The Catcher in the Rye — Sa… 51 Lord of the Flies — Golding 41 The Scarlet Letter — Hawtho… 40
Borda totals as a picture: the gaps show breadth of support, not just first choices.

Borda gives nineteen_eighty_four 93 points against mockingbird's 79. Instant-runoff reasons completely differently — eliminate the weakest option, retry until someone holds a majority — and its rounds array is that elimination story:

voting count show 20260728T183418670959Z_9235f748_book_preference_irv
Show command output
{
  "schema_version": "1.0",
  "command": "count show",
  "status": "ok",
  "argv": [
    "voting",
    "count",
    "show",
    "20260728T183418670959Z_9235f748_book_preference_irv"
  ],
  "data": {
    "created_at": "2026-07-28T14:34:18",
    "election_id": "book_preference",
    "exhausted_weight": 0.0,
    "id": "20260728T183418670959Z_9235f748_book_preference_irv",
    "method": "irv",
    "ranking": [
      {
        "option_id": "nineteen_eighty_four",
        "rank": 1,
        "status": "elected"
      },
      {
        "option_id": "mockingbird",
        "rank": 2,
        "status": "eliminated"
      },
      {
        "option_id": "catcher_rye",
        "rank": 3,
        "status": "eliminated"
      },
      "… 5 further entries elided …"
    ],
    "rounds": [
      {
        "eliminated": "lord_flies",
        "exhausted_weight": 0.0,
        "totals": [
          {
            "option_id": "nineteen_eighty_four",
            "total": 8.0
          },
          {
            "option_id": "mockingbird",
            "total": 4.0
          },
          "… 6 further entries elided …"
        ]
      },
      {
        "eliminated": "scarlet_letter",
        "exhausted_weight": 0.0,
        "totals": [
          {
            "option_id": "nineteen_eighty_four",
            "total": 8.0
          },
          {
            "option_id": "mockingbird",
            "total": 4.0
          },
          "… 5 further entries elided …"
        ]
      },
      {
        "eliminated": "animal_farm",
        "exhausted_weight": 0.0,
        "totals": [
          {
            "option_id": "nineteen_eighty_four",
            "total": 8.0
          },
          {
            "option_id": "mockingbird",
            "total": 4.0
          },
          "… 4 further entries elided …"
        ]
      },
      "… 4 further entries elided …"
    ],
    "scores": [
      {
        "option_id": "nineteen_eighty_four",
        "total": 12.0
      },
      {
        "option_id": "great_gatsby",
        "total": 6.0
      },
      {
        "option_id": "animal_farm",
        "total": 0.0
      },
      "… 5 further entries elided …"
    ],
    "settings": {
      "quota": "droop",
      "seats": 1,
      "tie_policy": "lexicographic"
    },
    "summary": {
      "exhausted_weight": 0.0,
      "invalid_ballots": 0,
      "total_valid_weight": 18.0,
      "valid_ballots": 18
    },
    "warnings": [],
    "winners": [
      "nineteen_eighty_four"
    ]
  },
  "warnings": [],
  "errors": [],
  "next_steps": []
}

Schulze (a Condorcet method) plays every option against every other; the pairwise matrix holds all 28 head-to-head margins, and voting plot pairwise makes it legible at a glance:

voting count show 20260728T183418671779Z_78e228ae_book_preference_schulze
Show command output
{
  "schema_version": "1.0",
  "command": "count show",
  "status": "ok",
  "argv": [
    "voting",
    "count",
    "show",
    "20260728T183418671779Z_78e228ae_book_preference_schulze"
  ],
  "data": {
    "created_at": "2026-07-28T14:34:18",
    "election_id": "book_preference",
    "id": "20260728T183418671779Z_78e228ae_book_preference_schulze",
    "method": "schulze",
    "pairwise": [
      {
        "a": "great_gatsby",
        "a_over_b": 9.0,
        "b": "mockingbird",
        "b_over_a": 9.0,
        "…": "3 further keys elided"
      },
      {
        "a": "great_gatsby",
        "a_over_b": 6.0,
        "b": "nineteen_eighty_four",
        "b_over_a": 12.0,
        "…": "3 further keys elided"
      },
      {
        "a": "great_gatsby",
        "a_over_b": 12.0,
        "b": "lord_flies",
        "b_over_a": 6.0,
        "…": "3 further keys elided"
      },
      "… 25 further entries elided …"
    ],
    "path_strengths": {
      "animal_farm": {
        "catcher_rye": 10.0,
        "great_gatsby": 0.0,
        "lord_flies": 13.0,
        "mice_men": 0.0,
        "…": "3 further keys elided"
      },
      "catcher_rye": {
        "animal_farm": 0.0,
        "great_gatsby": 0.0,
        "lord_flies": 0.0,
        "mice_men": 0.0,
        "…": "3 further keys elided"
      },
      "great_gatsby": {
        "animal_farm": 10.0,
        "catcher_rye": 11.0,
        "lord_flies": 12.0,
        "mice_men": 10.0,
        "…": "3 further keys elided"
      },
      "lord_flies": {
        "animal_farm": 0.0,
        "catcher_rye": 0.0,
        "great_gatsby": 0.0,
        "mice_men": 0.0,
        "…": "3 further keys elided"
      },
      "mice_men": {
        "animal_farm": 0.0,
        "catcher_rye": 11.0,
        "great_gatsby": 0.0,
        "lord_flies": 13.0,
        "…": "3 further keys elided"
      },
      "mockingbird": {
        "animal_farm": 12.0,
        "catcher_rye": 13.0,
        "great_gatsby": 0.0,
        "lord_flies": 14.0,
        "…": "3 further keys elided"
      },
      "nineteen_eighty_four": {
        "animal_farm": 13.0,
        "catcher_rye": 12.0,
        "great_gatsby": 12.0,
        "lord_flies": 16.0,
        "…": "3 further keys elided"
      },
      "scarlet_letter": {
        "animal_farm": 0.0,
        "catcher_rye": 0.0,
        "great_gatsby": 0.0,
        "lord_flies": 0.0,
        "…": "3 further keys elided"
      }
    },
    "ranking": [
      {
        "option_id": "nineteen_eighty_four",
        "rank": 1,
        "status": "elected"
      },
      {
        "option_id": "great_gatsby",
        "rank": 2,
        "status": "defeated"
      },
      {
        "option_id": "mockingbird",
        "rank": 3,
        "status": "defeated"
      },
      "… 5 further entries elided …"
    ],
    "rounds": [],
    "settings": {
      "quota": "droop",
      "seats": 1,
      "tie_policy": "lexicographic"
    },
    "summary": {
      "exhausted_weight": 0.0,
      "invalid_ballots": 0,
      "total_valid_weight": 18.0,
      "valid_ballots": 18
    },
    "warnings": [],
    "winners": [
      "nineteen_eighty_four"
    ]
  },
  "warnings": [],
  "errors": [],
  "next_steps": []
}
voting plot pairwise 20260728T183418671779Z_78e228ae_book_preference_schulze
Show command output
{
  "schema_version": "1.0",
  "command": "plot pairwise",
  "status": "ok",
  "argv": [
    "voting",
    "plot",
    "pairwise",
    "20260728T183418671779Z_78e228ae_book_preference_schulze"
  ],
  "data": {
    "result_id": "20260728T183418671779Z_78e228ae_book_preference_schulze",
    "method": "schulze",
    "path": ".voting/output/plots/pairwise_20260728T183418671779Z_78e228ae_book_preference_schulze.svg",
    "format": "svg"
  },
  "warnings": [],
  "errors": [],
  "next_steps": [
    "open .voting/output/plots/pairwise_20260728T183418671779Z_78e228ae_book_preference_schulze.svg"
  ]
}
Head-to-head margins — book_preference Green: row beats column (darker = larger margin). Red: row loses. Grey: tie. 1984 — Orwell The Great Gats To Kill a Mock Animal Farm — Of Mice and Me Lord of the Fl The Catcher in The Scarlet Le 1984 — Orwell 1984 — Orwell vs The Great Gatsby — Fitzgera…: +6 +6 1984 — Orwell vs To Kill a Mockingbird — Har…: +4 +4 1984 — Orwell vs Animal Farm — Orwell: +8 +8 1984 — Orwell vs Of Mice and Men — Steinbeck: +8 +8 1984 — Orwell vs Lord of the Flies — Golding: +14 +14 1984 — Orwell vs The Catcher in the Rye — Sa…: +6 +6 1984 — Orwell vs The Scarlet Letter — Hawtho…: +14 +14 The Great Gatsby — Fitzg The Great Gatsby — Fitzgera… vs 1984 — Orwell: -6 -6 The Great Gatsby — Fitzgera… vs To Kill a Mockingbird — Har…: +0 +0 The Great Gatsby — Fitzgera… vs Animal Farm — Orwell: +2 +2 The Great Gatsby — Fitzgera… vs Of Mice and Men — Steinbeck: +2 +2 The Great Gatsby — Fitzgera… vs Lord of the Flies — Golding: +6 +6 The Great Gatsby — Fitzgera… vs The Catcher in the Rye — Sa…: +4 +4 The Great Gatsby — Fitzgera… vs The Scarlet Letter — Hawtho…: +12 +12 To Kill a Mockingbird — To Kill a Mockingbird — Har… vs 1984 — Orwell: -4 -4 To Kill a Mockingbird — Har… vs The Great Gatsby — Fitzgera…: +0 +0 To Kill a Mockingbird — Har… vs Animal Farm — Orwell: +6 +6 To Kill a Mockingbird — Har… vs Of Mice and Men — Steinbeck: +2 +2 To Kill a Mockingbird — Har… vs Lord of the Flies — Golding: +10 +10 To Kill a Mockingbird — Har… vs The Catcher in the Rye — Sa…: +8 +8 To Kill a Mockingbird — Har… vs The Scarlet Letter — Hawtho…: +10 +10 Animal Farm — Orwell Animal Farm — Orwell vs 1984 — Orwell: -8 -8 Animal Farm — Orwell vs The Great Gatsby — Fitzgera…: -2 -2 Animal Farm — Orwell vs To Kill a Mockingbird — Har…: -6 -6 Animal Farm — Orwell vs Of Mice and Men — Steinbeck: +0 +0 Animal Farm — Orwell vs Lord of the Flies — Golding: +8 +8 Animal Farm — Orwell vs The Catcher in the Rye — Sa…: +2 +2 Animal Farm — Orwell vs The Scarlet Letter — Hawtho…: +4 +4 Of Mice and Men — Steinb Of Mice and Men — Steinbeck vs 1984 — Orwell: -8 -8 Of Mice and Men — Steinbeck vs The Great Gatsby — Fitzgera…: -2 -2 Of Mice and Men — Steinbeck vs To Kill a Mockingbird — Har…: -2 -2 Of Mice and Men — Steinbeck vs Animal Farm — Orwell: +0 +0 Of Mice and Men — Steinbeck vs Lord of the Flies — Golding: +8 +8 Of Mice and Men — Steinbeck vs The Catcher in the Rye — Sa…: +4 +4 Of Mice and Men — Steinbeck vs The Scarlet Letter — Hawtho…: +4 +4 Lord of the Flies — Gold Lord of the Flies — Golding vs 1984 — Orwell: -14 -14 Lord of the Flies — Golding vs The Great Gatsby — Fitzgera…: -6 -6 Lord of the Flies — Golding vs To Kill a Mockingbird — Har…: -10 -10 Lord of the Flies — Golding vs Animal Farm — Orwell: -8 -8 Lord of the Flies — Golding vs Of Mice and Men — Steinbeck: -8 -8 Lord of the Flies — Golding vs The Catcher in the Rye — Sa…: +0 +0 Lord of the Flies — Golding vs The Scarlet Letter — Hawtho…: +2 +2 The Catcher in the Rye — The Catcher in the Rye — Sa… vs 1984 — Orwell: -6 -6 The Catcher in the Rye — Sa… vs The Great Gatsby — Fitzgera…: -4 -4 The Catcher in the Rye — Sa… vs To Kill a Mockingbird — Har…: -8 -8 The Catcher in the Rye — Sa… vs Animal Farm — Orwell: -2 -2 The Catcher in the Rye — Sa… vs Of Mice and Men — Steinbeck: -4 -4 The Catcher in the Rye — Sa… vs Lord of the Flies — Golding: +0 +0 The Catcher in the Rye — Sa… vs The Scarlet Letter — Hawtho…: +0 +0 The Scarlet Letter — Haw The Scarlet Letter — Hawtho… vs 1984 — Orwell: -14 -14 The Scarlet Letter — Hawtho… vs The Great Gatsby — Fitzgera…: -12 -12 The Scarlet Letter — Hawtho… vs To Kill a Mockingbird — Har…: -10 -10 The Scarlet Letter — Hawtho… vs Animal Farm — Orwell: -4 -4 The Scarlet Letter — Hawtho… vs Of Mice and Men — Steinbeck: -4 -4 The Scarlet Letter — Hawtho… vs Lord of the Flies — Golding: -2 -2 The Scarlet Letter — Hawtho… vs The Catcher in the Rye — Sa…: +0 +0
Every head-to-head from the real ballots: a cell is the row's margin over the column. nineteen_eighty_four's top row is solid green — it beats all seven rivals directly, which is why all five Condorcet-family methods (schulze, copeland, minimax, ranked_pairs, kemeny_young) must elect it.

The rest tell the same story in different dialects: copeland scores those pairings as wins minus losses, kemeny_young searches for the ordering that agrees with the most pairwise judgments, bucklin keeps adding voters' next choices until someone crosses half, runoff stages a two-option finale (nineteen_eighty_four vs mockingbird), and fptp throws away everything but the first line. Their envelopes are all in count list:

voting count list
Show command output
{
  "schema_version": "1.0",
  "command": "count list",
  "status": "ok",
  "argv": [
    "voting",
    "count",
    "list"
  ],
  "data": {
    "results": [
      {
        "created_at": "2026-07-28T14:34:18",
        "election_id": "book_preference",
        "id": "20260728T183418670409Z_f264c771_book_preference_borda",
        "method": "borda",
        "…": "7 further keys elided"
      },
      {
        "created_at": "2026-07-28T14:34:18",
        "election_id": "book_preference",
        "exhausted_weight": 0.0,
        "id": "20260728T183418670959Z_9235f748_book_preference_irv",
        "…": "8 further keys elided"
      },
      {
        "created_at": "2026-07-28T14:34:18",
        "election_id": "book_preference",
        "exhausted_weight": 0.0,
        "id": "20260728T183418671304Z_3c6e721b_book_preference_stv",
        "…": "9 further keys elided"
      },
      "… 9 further entries elided …"
    ]
  },
  "warnings": [],
  "errors": [],
  "next_steps": []
}

And the thesis of this whole page, as one picture — voting plot methods grids every option's finishing position under every saved count:

voting plot methods --election book_preference
Show command output
{
  "schema_version": "1.0",
  "command": "plot methods",
  "status": "ok",
  "argv": [
    "voting",
    "plot",
    "methods",
    "--election",
    "book_preference"
  ],
  "data": {
    "election": "book_preference",
    "results": 12,
    "methods": [
      "borda",
      "bucklin",
      "copeland",
      "… 9 further entries elided …"
    ],
    "path": ".voting/output/plots/methods_book_preference.svg",
    "format": "svg"
  },
  "warnings": [],
  "errors": [],
  "next_steps": [
    "open .voting/output/plots/methods_book_preference.svg"
  ]
}
Finishing positions across counting methods Cell = finishing position under that method (1 = winner). A solid top row means the method does not change the outcome. borda irv stv schulze copeland minimax ranked_pairs kemeny_young bucklin runoff fptp simple_majority* * declared no winner (its ranking is shown for comparison) 1984 — Orwell 1 1 1 1 1 1 1 1 1 1 1 1 To Kill a Mockingbird — Har… 2 2 2 3 3 2 3 3 4 2 2 2 The Great Gatsby — Fitzgera… 3 8 8 2 2 3 2 2 2 5 4 4 Animal Farm — Orwell 5 5 5 4 4 4 4 4 3 3 5 5 The Catcher in the Rye — Sa… 6 3 3 7 7 5 7 6 5 4 3 3 Of Mice and Men — Steinbeck 4 4 4 5 5 6 5 5 6 7 6 6 Lord of the Flies — Golding 7 7 7 6 6 7 6 7 8 6 7 7 The Scarlet Letter — Hawtho… 8 6 6 8 8 8 8 8 7 8 8 8
The answer to the tutorial's question. The unbroken dark top row is the finding: no counting method changes the winner (the asterisk marks simple_majority, which ranks but declines to declare). The shuffling in the middle rows is where method choice does matter.

Every method that names a winner names 1984. That unanimity is itself the finding. Orwell's novel holds 8 of 18 first preferences, the top Borda score, and beats every rival head-to-head. When a candidate dominates like this, the choice of method cannot change the outcome; method choice matters exactly when support is fragmented, and these ballots are not fragmented at the top. (Beneath the winner the orderings do shuffle — compare the ranking arrays across the saved results.)

What this page cannot claim. Eighteen self-selected respondents are a poll of those eighteen people, not of readers in general. The demonstration is methodological — one auditable dataset, many counting rules, disclosed outputs — not a literary verdict.

07 Where everything lives

Every entity the project holds is a small JSON file under the project's .voting/ directory: options, voters, ballots (append-only records; re-imports warn ballot_overwritten), and one saved result per count, so comparisons never overwrite each other — all inspectable with nothing but cat. next closes the loop:

voting next
Show command output
{
  "schema_version": "1.0",
  "command": "next",
  "status": "ok",
  "argv": [
    "voting",
    "next"
  ],
  "data": {
    "phase": "done",
    "counts": {
      "options": 8,
      "voters": 18,
      "elections": 1,
      "ballots": 18,
      "results": 12
    },
    "recommendation": "voting count list",
    "reason": "View results",
    "checklist": [
      "Review results: `voting count list` and `voting count show <result_id>`",
      "Run additional methods for comparison: `voting count run <election_id> --method <method>`"
    ]
  },
  "warnings": [],
  "errors": [],
  "next_steps": [
    "voting count list",
    "voting count run <election_id> --method <method>"
  ]
}
NeedCommand family
Discover the workflowversion, capabilities, status, next, docs list
Define options and votersoption add|import, voter add, election add|add-option|open
Collect ballotsballot rank|cast|approve|score, survey generate, survey humanize|publish|email|responses
Import and auditballot import --from|--from-results|--from-coop [--register-voters], ballot validate, ballot list
Count and comparecount run [--method], count list, count show

Exact options and defaults belong to voting <command> --help; the README's generated command reference lists all 51 commands and is enforced against the CLI by tests/test_contract_sync.py.

That is the whole loop. The natural next step is to run it on a question you actually care about: define your options, publish the survey to the people whose answer matters (or let AI personas vote via survey generate and ep run), and — before reading any winner — check ballot validate, then count it more than one way. If all the methods agree, you have a robust answer. If they disagree, you have something more interesting.