Building a Ballot Counter in Python

A numbered spec turned into a working program: reading it as a data flow, choosing a dictionary over parallel lists, seeding and guarding the counting loop, validating input without swallowing your own bugs, handling ties correctly, and splitting the code so the logic can be tested without typing votes by hand.

Subject: IT Support & Networking · 64 slides · code lesson

Open the interactive version of this deck · Homework for this lesson

What this lesson covers

The lesson, slide by slide

1. Building a Ballot Counter in Python

Title

IT · Programming

Eight spec steps, one program, and the grader that runs it on inputs you have not seen

2. What you will be able to do

Objectives

The assignment is short but unforgiving: the grader runs your program on inputs you never see, so the program has to be right for reasons you can explain, not because it happened to work once.

The Python Tutorial — python.org — the reference this deck follows

3. Read the Spec as a Data Flow

Section

Section 1

4. Before any code

Warm-up

Two minutes with the spec and a pen.

Discussion prompt

For any program like this one, what are the three questions you should answer before typing a single line?

Hint: Input, state, output.

Answer:

What comes in, what shape does it have, and where does it come from.

What has to be remembered while the program runs, and in what container.

What goes out, in exactly what format, since a grader usually compares your output character by character.

Answer those three and the code is mostly transcription.

5. Eight numbered steps are really five stages

Concept

A spec written as eight steps is usually one data flow with the stages spelled out. Grouping them makes the program's shape obvious.

Read votes, reject the invalid ones, count the rest, find the highest count, print the result. Everything in the spec is one of those five.

CS50x — Harvard's introduction to computer science, the plurality and runoff problem sets — the plurality problem set uses exactly this shape

6. The whole program, before any syntax

Picture it

Five boxes. Each becomes one function.

Figure (svg): A pipeline of five stages: read votes, validate, tally, find max, and report

Write the boxes on paper first; the code follows the arrows.

The single biggest cause of a tangled program is starting to type before this picture exists.

7. Which stage does each spec line belong to?

Sorting

The spec is deliberately written out of order.

Sort into buckets

Sort each requirement into its stage.

input
prompt for the number of voters; print 'Invalid vote.' and do not count it
state
keep a running total for each candidate
output
if two candidates tie, print both names; print each candidate and their total
in
Anything about getting data from the user, including rejecting it. Validation belongs with input because a rejected vote must never reach the tally.
state
Anything the program has to remember between one vote and the next. There is only one such thing here, and it is the tally.
out
Anything about what is printed, including how ties are presented. Format details live here and graders check them exactly.

Notice how little state there is. One dictionary is the entire memory of this program.

8. Choosing the Container

Section

Section 2

9. A dictionary maps a name to its count

Concept

You need to look up a candidate by name and change their number. That is precisely what a dictionary does, in one step, no searching.

dictionary — A container that maps keys to values. Looking up a key takes the same time whether there are three entries or three million.

Python Standard Library — Mapping Types (dict) — the mapping type reference

10. Names on the left, counts on the right

Picture it

Three entries, each one a key pointing at a value.

Figure (svg): A dictionary drawn as three key boxes with arrows to three value boxes, mapping Alice to three, Bob to two and Charlie to one

The key is the candidate; the value is the running total.

11. Why not two parallel lists?

Concept

The alternative is one list of names and one list of counts, kept in the same order. It works, and it is how you would do it in C.

names  = ['Alice', 'Bob', 'Charlie']
counts = [0, 0, 0]

# to add a vote you must first find the position
i = names.index('Bob')
counts[i] += 1
operationwith two listswith a dictionary
add a votesearch for the index, then incrementone increment
riskthe lists can drift out of stepimpossible; one structure
unknown nameindex raises ValueErrorhandled with a membership test
lines of codemorefewer

The parallel-list version is not wrong. It is just two things to keep synchronised where one would do, and that is where bugs come from.

The Python Tutorial — python.org — Data Structures

12. What does this print?

Prediction

A dictionary with three entries, and a lookup for a name that is not one of them.

Predict first

tally = {'Alice': 3}; print(tally['Bob'])

  • 0
  • None
  • It raises KeyError
  • An empty string

Correct: It raises KeyError.

Why: Square-bracket lookup on a missing key is an error, not a default. This matters here because a vote for an unknown candidate would crash the program rather than being rejected politely, which is exactly the case the grader will test.

13. Three ways to handle a missing key

Concept

Each is correct in a different situation, and the assignment needs the first one.

# 1. membership test: reject anything not on the ballot
if name in tally:
    tally[name] += 1
else:
    print('Invalid vote.')

# 2. get with a default: read without inserting
count = tally.get(name, 0)

# 3. setdefault: insert on first sight, for an open ballot
tally.setdefault(name, 0)
tally[name] += 1
formmissing key becomesuse it when
name in tallyrejectedthe candidate list is fixed, as here
tally.get(name, 0)reads as 0, not storedyou are only reading a count
tally.setdefault(name, 0)created with 0any name is allowed, like a write-in

This assignment has a fixed ballot, so the membership test is the right one. The others would silently accept a misspelling as a new candidate.

Python Standard Library — Mapping Types (dict) — dict methods

14. Which container fits each job?

Discrimination

Choosing the container is most of the design.

Sort into buckets

List, or dictionary?

list
the votes in the order they were cast; the names on the ballot; which candidates are tied for the lead
dictionary
the running total per candidate
list
Order matters, or you just need a sequence you will walk through. Nothing is being looked up by name.
dict
You need to find something by name and change it. That is a lookup, and a lookup is what a dictionary is for.

15. The Counting Loop

Section

Section 3

16. Worked example: tally a list of votes

Worked example

Start from a list, not from input. A function that takes a list can be tested; a function that reads from the keyboard cannot.

Build the tally with every candidate at zero

Why: Starting from zero means a candidate with no votes still appears in the output, which the spec requires.

Walk the votes once

Why: One pass, one increment. There is never a reason to loop twice here.

Reject anything not on the ballot

Why: The membership test both validates and protects the increment from a KeyError.

def tally_votes(votes, candidates):
    tally = {name: 0 for name in candidates}
    for vote in votes:
        if vote in tally:
            tally[vote] += 1
        else:
            print('Invalid vote.')
    return tally
votevalid?AliceBobCharlie
start-000
Aliceyes100
Bobyes110
Aliceyes210
Daveno210
Charlieyes211
Aliceyes311

Verify: that the totals sum to the number of valid votes

Why: Three plus one plus one is five, and six votes were offered with one rejected. If the sum does not match, a vote was counted twice or lost.

17. The tally growing, one vote at a time

Picture it

Six votes, six increments, one pass.

Figure (svg): Six rows showing each incoming vote on the left and the state of the tally after it on the right

Nothing is recomputed; each vote adds exactly one.

18. What stays true on every pass?

Invariant

Step through the loop and watch two quantities.

Step through it

What does the gap between the two numbers tell you, and what would it mean if the gap were negative?

  1. Both start at zero.
  2. A valid vote raises both by one.
  3. So does the next one.
  4. A rejected vote raises the first and not the second. The gap is the invalid count.
  5. At the end, the difference is exactly the number of rejected votes.

A negative gap is impossible, so if you ever see one, the loop is counting something twice. This is a good assertion to add while debugging.

19. Trap: building the tally as you go

Trap

The trap

A version that adds candidates the first time it sees them.

Annotate

  • Every name creates an entry, so a typo becomes a candidate.
  • A candidate who received no votes never appears, and the spec asks for all of them.
  • Nothing is ever reported as invalid, because nothing can be invalid.

Pass your own test, fail the grader's

Why: Your test data has no typos in it. The grader's does.

The fix

Seed the tally from the ballot, then only ever increment.

tally = {name: 0 for name in candidates}
for vote in votes:
    if vote in tally:
        tally[vote] += 1
    else:
        print('Invalid vote.')
inputseeded versiongrow-as-you-go version
'Alice'Alice becomes 1Alice becomes 1
'alice'Invalid vote.a second candidate appears
candidate with no votesreported as 0missing entirely

The ballot is fixed, so the keys should be too

Why: Deciding the keys up front is what makes validation possible at all.

20. Find the bug in this loop

Error analysis

It runs, raises nothing, and gives the wrong answer on every input with a repeat in it.

Annotate

  • This assigns 1 rather than adding 1, so a second vote for the same candidate changes nothing.
  • Every candidate who received at least one vote ends on exactly 1, and the winner is whoever sorts first.
  • Correct: tally[vote] += 1, which reads the current value before storing.
votesbuggy resultcorrect result
AliceAlice 1Alice 1
Alice, AliceAlice 1Alice 2
Alice, Bob, AliceAlice 1, Bob 1Alice 2, Bob 1

A single-vote test would pass. This is why the test list in section six has a repeat in it.

21. Complete the increment

Fill the middle

The guard is written; the body is missing.

Fill in the blanks

for vote in votes:
if vote in tally:
tally[vote] += 1
else:
print('Invalid vote.')

Why: The augmented assignment reads the current value, adds one, and stores it back, all in one expression. Writing it the long way is equally correct and four characters longer.

22. Pattern: the counting loop

Pattern

Four lines, and every one of them is load-bearing.

  1. Seed the tally from the ballot, so every candidate exists at zero before any vote arrives.
  2. Iterate the votes exactly once. Two passes means two chances to disagree with yourself.
  3. Guard with a membership test, which validates and protects the increment in the same breath.
  4. Increment, never assign. Assignment silently caps every candidate at one.
  5. Return the tally rather than printing it, so the function stays testable.

Any counting problem you meet later, in any language, is this loop with a different key.

Python Standard Library — collections.Counter — the standard library ships this loop as Counter, once you are allowed to use it

23. Input and Validation

Section

Section 4

24. input always returns a string

Concept

Whatever the user types, input hands you text. A number typed at the prompt is the characters of a number, not the number.

n = input('Number of voters: ')
print(n + 1)          # TypeError: can only concatenate str

n = int(input('Number of voters: '))
print(n + 1)          # works
expressiontypevalueresult
input()str'6'text
input() + 1--TypeError
int(input())int6a number
int('six')--ValueError

The last row is the one graders test: a non-numeric answer to a numeric prompt.

The Python Tutorial — python.org — Input and Output

25. What happens on bad input?

Prediction

The program calls int(input()) and the user types 'six'.

Predict first

What does the program do?

  • Prints 0 and carries on
  • Raises ValueError and stops
  • Asks again automatically
  • Treats it as the number 6

Correct: Raises ValueError and stops.

Why: int refuses text it cannot parse and raises immediately. If the spec says to keep asking, the fix is a try and except inside a while loop. If it does not, an unhandled crash may be acceptable, but knowing which is which is part of reading the spec.

26. Worked example: a prompt that will not accept nonsense

Worked example

Loop until the input parses, and only then leave the loop.

Loop forever, and break out on success

Why: A while-true loop with a break is the idiomatic shape here; a flag variable is more code doing the same thing.

Attempt the conversion inside a try block

Why: Only the risky line goes inside. Putting the whole body in there hides real bugs behind the same handler.

Handle exactly the exception you expect

Why: Catching a bare exception would also swallow a keyboard interrupt and every typo in your own code.

def ask_int(prompt):
    while True:
        try:
            return int(input(prompt))
        except ValueError:
            print('Please enter a whole number.')
user typesint() doesloop
'six'raises ValueErrorprints the message, asks again
''raises ValueErrorprints the message, asks again
'6'returns 6returns, leaving the loop
' 6 'returns 6returns; int strips whitespace

Verify: that the last row surprises you in the right direction

Why: int tolerates surrounding whitespace but not an empty string. Knowing which conversions are forgiving is worth more than guessing.

27. A run with one rejected vote

Picture it

This is the output shape the grader compares against.

Figure (svg): A terminal session prompting for six votes, printing Invalid vote for one of them, and reporting Alice as the winner

Match the wording and the punctuation exactly.

Graders usually compare output character by character. A trailing space or a missing full stop fails a program that is otherwise perfect.

28. Trap: catching every exception

Trap

The trap

A defensive-looking handler.

Annotate

  • A bare except catches everything, including typos in your own function names.
  • So a NameError inside tally_votes prints the same friendly message as bad user input.
  • You lose the traceback, which was the only thing that would have told you where the real bug is.

Spend an afternoon on a bug the traceback would have named

Why: This is the single most expensive habit a beginner can pick up.

The fix

Catch the specific exception, around the specific line.

try:
    n = int(input('Number of voters: '))
except ValueError:
    print('Please enter a whole number.')

tally = tally_votes(read_votes(n), candidates)
errorbare exceptspecific except
user typed 'six'friendly messagefriendly message
typo in a function namefriendly message, bug hiddenfull traceback, bug named
Ctrl+Cswallowedprogram exits as expected

Keep the risky line alone inside the try

Why: Everything else moves out, so nothing else can be caught by accident.

29. Finding the Winner

Section

Section 5

30. The maximum is a number; the winner is a list

Concept

Ask for the highest count first, then ask who reached it. Doing it in that order handles ties for free.

best = max(tally.values())
winners = [name for name, count in tally.items() if count == best]
stepvalue with a clear winvalue with a tie
tallyAlice 3, Bob 2, Charlie 1Alice 3, Bob 3, Charlie 1
max of the values33
winners list['Alice']['Alice', 'Bob']
what to printWinner: AliceWinner: Alice and Bob

Python Standard Library — Mapping Types (dict) — dict.items and dict.values

31. Everyone who reaches the maximum

Picture it

Two candidates on three votes each.

Figure (svg): Three bars showing Alice and Bob tied on three votes with Charlie on one, with both leaders marked as reaching the maximum

The maximum is 3; two candidates reach it.

32. Why the answer has to be a list

Picture it

Scan the tally once, keeping everyone whose count equals the maximum.

Figure (svg): Three candidate rows with the two on the maximum count highlighted and the third marked as below it

Nothing in the data guarantees exactly one winner.

A program that returns a single name is not wrong on your test data. It is wrong on the grader's.

33. Worked example: report the result

Worked example

Print every candidate with their total, then the winner or winners.

Sort by count descending, then by name

Why: Sorting by two keys at once keeps the output stable, so two runs on the same data always print the same order.

Print the table

Why: An f-string with a width specifier lines the columns up without any manual padding.

Print the winners, joined

Why: Joining a list handles one name and three names with the same line of code.

for name, count in sorted(tally.items(), key=lambda kv: (-kv[1], kv[0])):
    print(f'{name:<10}{count}')

best = max(tally.values())
winners = [n for n, c in tally.items() if c == best]
print('Winner: ' + ' and '.join(sorted(winners)))
candidatecountprinted line
Alice3Alice 3
Bob2Bob 2
Charlie1Charlie 1

Verify: by running it twice on the same input

Why: The order must be identical both times. If it is not, the sort key is incomplete and the grader may see a different order than you did.

34. The formatted output, character for character

Picture it

This is what the grader compares against.

Figure (svg): A terminal showing the tally printed in aligned columns followed by the winner line

The width specifier does the padding; never build columns with manual spaces.

If the spec shows an example of the output, copy its spacing exactly. That example is the specification.

35. Decoding the sort key

Notation

One line does a lot here, and copying it without understanding it means you cannot change it later.

Annotate

  • tally.items() yields pairs, so each kv is a two-element tuple of name and count.
  • The key returns a tuple, and Python sorts tuples left to right: count first, name only as a tie-break.
  • The minus sign flips the count so the highest comes first, without needing reverse=True, which would also reverse the names.
key returned fortuplesorts
Alice, 3(-3, 'Alice')first
Bob, 2(-2, 'Bob')second
Charlie, 1(-1, 'Charlie')third

Using reverse=True instead would put the counts in the right order and the tied names in the wrong one, which is the subtle version of this bug.

Python HOWTO — Sorting Techniques — sorting by multiple keys

36. Which winner logic is correct?

Elimination

The tally is Alice 3, Bob 3, Charlie 1.

Eliminate the wrong options

Which one reports both leaders?

  • A. best = max(tally.values()); winners = [n for n, c in tally.items() if c == best]
  • B. winner = max(tally, key=tally.get)
  • C. winner = sorted(tally.items(), key=lambda kv: kv[1])[-1]
  • D. winners = [n for n in tally if tally[n] > 1]

Survives elimination: A

Why: Compute the maximum first, then collect everyone equal to it. The other three all collapse the answer to a single winner at the moment they are evaluated, which is the exact case the spec asks you to handle.

37. Check: what does max return here?

Check

Solve it on paper before you click.

Check your understanding

tally = {'Alice': 3, 'Bob': 3, 'Charlie': 1}. What does max(tally, key=tally.get) return?

  • A. 'Alice', silently ignoring the tie (correct)
  • B. ['Alice', 'Bob']
  • C. 3
  • D. It raises an error because of the tie

Answer: A

Why: max returns one item. With the key function it compares counts, and on a tie it keeps the first one it encountered. The tie is not detected and not reported, which is why this shape is wrong for this assignment.

Why B tempts people
max never returns a list. Producing all the winners needs a separate pass comparing against the maximum.
Why C tempts people
That is max(tally.values()), the highest count. Iterating a dictionary yields its keys, so this form returns a name.
Why D tempts people
Ties are not errors. Python resolves them by order, quietly, which is precisely the danger.

38. Functions and Testing

Section

Section 6

39. A function that reads input cannot be tested

Concept

Split the program so the logic takes arguments and returns values, and only a thin outer layer touches input and print.

Then the interesting part can be called from a test with a known list, and you never type six votes by hand again.

The Python Tutorial — python.org — Defining Functions

40. The order to build it in

Picture it

Logic first, input last. Reversing this is why testing feels impossible.

Figure (svg): Four steps: write a function taking a list, call it from a test, compare against an expected dictionary, and only then wire it to input

The keyboard is the last thing you connect, not the first.

41. The whole program

Concept

Five functions matching the five stages, and a main that wires them together.

CANDIDATES = ['Alice', 'Bob', 'Charlie']

def tally_votes(votes, candidates):
    tally = {name: 0 for name in candidates}
    for vote in votes:
        if vote in tally:
            tally[vote] += 1
        else:
            print('Invalid vote.')
    return tally

def winners_of(tally):
    best = max(tally.values())
    return sorted(n for n, c in tally.items() if c == best)

def report(tally):
    for name, count in sorted(tally.items(), key=lambda kv: (-kv[1], kv[0])):
        print(f'{name:<10}{count}')
    print('Winner: ' + ' and '.join(winners_of(tally)))

def main():
    n = int(input('Number of voters: '))
    votes = [input('Vote: ') for _ in range(n)]
    report(tally_votes(votes, CANDIDATES))

if __name__ == '__main__':
    main()
functiontakesreturnstouches input or print?
tally_votesa list and the ballota dictprints rejections only
winners_ofa dicta sorted listno
reporta dictnothingprints
mainnothingnothingreads input

Two of the four are pure: same input, same output, no side effects. Those two are the ones worth testing, and they are where the marks are.

PEP 8 — Style Guide for Python Code — naming and layout conventions used here

42. Worked example: a test you can actually run

Worked example

No typing votes in. Just call the function.

Write the input and the expected output side by side

Why: If you cannot write the expected output, you do not yet understand the requirement.

Assert equality between dictionaries

Why: Two dictionaries compare equal when they have the same keys and values, regardless of insertion order.

Add the edge cases the grader will use

Why: An empty vote list, an all-invalid list, and a tie. Those three catch nearly everything.

def test_tally():
    votes = ['Alice', 'Bob', 'Alice', 'Dave', 'Charlie', 'Alice']
    got = tally_votes(votes, CANDIDATES)
    assert got == {'Alice': 3, 'Bob': 1, 'Charlie': 1}

    assert tally_votes([], CANDIDATES) == {'Alice': 0, 'Bob': 0, 'Charlie': 0}
    assert winners_of({'Alice': 3, 'Bob': 3, 'Charlie': 1}) == ['Alice', 'Bob']
    print('all tests passed')
caseinputexpected
normalsix votes, one invalidAlice 3, Bob 1, Charlie 1
emptyno votesevery candidate on 0
tiea tally with two on 3both names, sorted

Verify: by breaking the code on purpose

Why: Change the increment to add two and re-run. If the test still passes, the test is not testing what you think it is.

43. What a failing test actually tells you

Picture it

The traceback names the line, the value, and the call that got there. Read it from the bottom up.

Figure (svg): A Python traceback ending in a KeyError for the name Dave, with the failing line inside tally_votes highlighted

Bottom line: what went wrong. Line above it: exactly where.

Beginners read tracebacks from the top and give up. The last two lines are the answer, and everything above them is the route taken to get there.

44. Explain why the test list has no input() in it

Explain it

Two sentences, out loud.

Discussion prompt

Why is it worth restructuring the program so the logic never calls input?

Hint: How many times will you retype six votes before you stop bothering?

Answer:

Because a function that reads the keyboard can only be exercised by a human typing, so it gets tested once and then never again.

A function that takes a list can be exercised a hundred times a second, which means you can afford to check every edge case every time you change anything.

The Python Tutorial — python.org — Defining Functions

45. Fill in the gaps in the design

Comparison

The four functions, and what makes each testable or not.

Comparison matrix

functionpure?how you would test it
tally_votesnearly; it prints rejectionscall it with a list, compare the dict
winners_ofyescall it with a tally, compare the list
reportno; it printscapture stdout, or test the pieces it calls
mainno; it reads inputrun the program by hand, once

The rule this table is teaching: push the impure parts to the edges, and keep the middle pure.

46. When It Misbehaves

Section

Section 7

47. Read the traceback from the bottom

Concept

The last line names the error and the offending value. The line just above it is the code that raised it. Everything higher up is how execution got there.

Two lines answer nearly every question you have. The rest is context you only need when the two lines are not enough.

The Python Tutorial — python.org — Errors and Exceptions

48. Match the error to its cause

Matching

These five cover almost every failure in a program of this shape.

Match the pairs

  • l1. KeyError: 'Dave'
  • l2. TypeError: can only concatenate str
  • l3. ValueError: invalid literal for int()
  • l4. ValueError: max() arg is an empty sequence
  • l5. IndentationError
  • r1. square-bracket lookup on a name not in the tally
  • r2. adding a number to the string that input returned
  • r3. the user typed something that is not a whole number
  • r4. the candidate list is empty, so there is nothing to take a maximum of
  • r5. a block is indented inconsistently, often tabs mixed with spaces

Why: Every one of these names its own cause once you know the vocabulary. KeyError is always a missing key; ValueError is always a value of the right type but the wrong content. Learning the five saves more time than any debugging technique.

49. Trap: debugging by adding print statements everywhere

Trap

The trap

Something is wrong, so prints go in.

Annotate

  • Six votes now produce eighteen lines of output, and the real message is buried.
  • Half of these will be left in when the file is submitted.

Trade one confusing problem for a wall of text

Why: Print debugging works, but scattering it is what makes it painful.

The fix

One print, at the one place the state changes, showing only what changed.

for vote in votes:
    if vote in tally:
        tally[vote] += 1
    else:
        print('rejected:', repr(vote))
techniqueoutput volumefinds the bug
print everythingeighteen lineseventually
print only the anomalyone lineimmediately
assert an invariantsilent until it breaksat the exact moment

Use repr rather than str when printing input

Why: It shows the quotes and any stray whitespace, which is exactly the class of bug that makes a valid-looking vote get rejected.

50. Why does repr matter here?

Socratic

A vote that looks correct is being rejected.

Discussion prompt

The user typed Alice and the program says invalid. What would repr show that print would not?

Hint: What is invisible when you print a string without its quotes?

Answer:

The quotes and the whitespace. print shows Alice; repr shows 'Alice ' with a trailing space, or 'alice' in lower case.

A trailing space from a copy-paste is the single most common cause of a valid-looking value failing a membership test.

Which suggests the fix the spec probably wants anyway: strip the input, and decide explicitly whether the comparison is case sensitive.

51. Where It Goes Next

Section

Section 8

52. Plurality is the easy half

Concept

Counting first preferences and taking the highest is plurality. It is what this assignment asks for, and it is the simplest voting rule there is.

The follow-up assignment is usually runoff, where each ballot is a ranked list and the lowest candidate is eliminated and their ballots redistributed until someone passes half.

CS50x — Harvard's introduction to computer science, the plurality and runoff problem sets — plurality then runoff, in that order

53. Plurality against runoff

Picture it

The counting loop is identical. What changes is what happens after it.

Figure (svg): A five step flow contrasting plurality, which counts once, with runoff, which counts, eliminates the lowest, redistributes and repeats

Runoff is your tally function, called in a loop.

Which is the real reason to write tally_votes as a function that takes a list: runoff calls it repeatedly, on a shrinking candidate set.

54. Two voting rules, weighed

Trade off

Fill in what each one costs.

Comparison matrix

pluralityrunoff
ballot shapeone namea ranked list
passes of the tallyoneone per elimination round
can elect someone most voters dislikeyes, with a split fieldmuch less easily, since it needs a majority
extra code you neednoneelimination and redistribution

Note the third row: it is the actual reason runoff exists, and it is worth understanding before writing the code that implements it.

55. Which claim about the tally function holds?

Two truths and a lie

Thinking ahead to the runoff version.

Eliminate the wrong options

Which is true?

  • A. Because tally_votes takes the candidate list as an argument, runoff can call it again with the eliminated candidate removed.
  • B. Runoff needs a completely different counting function.
  • C. Runoff can reuse the tally as-is without recounting.
  • D. Ties cannot happen in runoff.

Survives elimination: A

Why: Passing the candidate list in rather than reading a global is what makes the function reusable across rounds. That single design choice is the difference between runoff being a small extension and a rewrite.

56. How sure are you?

Commit first

Answer, then rate your confidence honestly.

Predict first

Six votes: Alice, Alice, Bob, Bob, Charlie, Charlie. Under plurality, who wins?

  • Alice
  • Nobody; it is a three-way tie
  • Charlie
  • The program crashes

Correct: Nobody; it is a three-way tie.

Why: All three candidates have two votes, so the maximum is 2 and all three reach it. A program that returns a single name reports Alice and is wrong, which is exactly the case the winners list exists to handle.

57. Pattern: the whole assignment on one card

Pattern

Eight spec steps, five stages, four functions, three tests.

  1. Draw the data flow before typing. Input, state, output.
  2. Seed the tally from the ballot, never grow it from the votes.
  3. Validate with a membership test, which also protects the increment.
  4. Compute the maximum first, then collect everyone who reaches it. The answer is a list.
  5. Keep input out of the logic, so the logic can be tested.
  6. Match the required output text exactly, punctuation included.
symptomcause, nearly always
KeyError on a votesquare-bracket lookup on a name not in the tally
a candidate missing from the outputthe tally was grown from votes instead of seeded
a tie reports one namemax used on the dict instead of on the values
TypeError adding to input()the string was never converted with int
a mysterious 'something went wrong'a bare except swallowing your own bug

CS50x — Harvard's introduction to computer science, the plurality and runoff problem sets — the same failure modes, in the plurality problem set

58. Order the build

Ranking

Build it in this order and you will never be debugging two things at once.

Put in order

  1. Write the data flow on paper
  2. Write tally_votes taking a list
  3. Write a test calling it with a known list
  4. Write winners_of and report
  5. Write main, wiring input to the tested functions

Why: The test comes third, before the rest of the program exists, because from that point on every later change is checked automatically. Writing main first is the natural instinct and it is what makes the last hour of the assignment miserable.

59. What does the spec not say?

Missing information

The spec says: read the votes, count them, print each candidate's total, print the winner.

Discussion prompt

Name three things the grader will test that the spec does not mention.

Hint: Think about the inputs a malicious grader would choose.

Answer:

What happens on a tie. Almost every spec omits it and almost every grader tests it.

What happens with zero voters, where max of an empty sequence raises ValueError.

Whether the candidate list is case sensitive, and what an unknown name should print.

When a spec is silent, pick the behaviour that cannot crash, and say in a comment which choice you made and why.

60. What happens with zero voters?

Edge cases

The user enters 0 at the first prompt.

Discussion prompt

Trace the program. Where does it break, and what is the smallest fix?

Hint: Which sequence is actually empty: the votes, or the candidates?

Answer:

The tally is built with every candidate on zero, which is fine. Then max of the values returns 0, which is also fine, and every candidate ties for the win.

The genuine crash case is max on an empty sequence, which happens only if the candidate list itself is empty.

The smallest honest fix is a guard: if there are no candidates, say so and stop. Do not silently print a winner that does not exist.

Python Standard Library — Mapping Types (dict) — max raises ValueError on an empty iterable

61. Check: the tie case

Check

Solve it on paper before you click.

Check your understanding

With the tally Alice 3, Bob 3, Charlie 1, what does winners_of return?

  • A. ['Alice', 'Bob'] (correct)
  • B. 'Alice'
  • C. ['Alice', 'Bob', 'Charlie']
  • D. 3

Answer: A

Why: The maximum count is 3, and two candidates reach it, so the comprehension keeps both. Sorting makes the order predictable so the output is the same on every run.

Why B tempts people
A single name is what max on the dictionary would give, and it is exactly the behaviour the list comprehension exists to avoid.
Why C tempts people
Charlie has one vote, which is below the maximum. Only equality with the maximum qualifies.
Why D tempts people
That is the maximum count itself, not the names that achieved it.

62. Exit ticket

Exit ticket

One honest answer, and it decides what the next session opens with.

Predict first

Which of these is still shakiest?

  • Choosing between a list and a dictionary
  • Writing the loop with the validation guard
  • Handling bad input without crashing
  • Getting ties right
  • Splitting the program into testable functions

Correct: Whichever you named is where we start.

Why: All five are drillable in about fifteen minutes each, and the last one is the highest leverage: once the logic is testable, every other item on the list becomes something you can check instead of something you have to be careful about.

63. Rebuild the program from the data flow

Connect it up

Blank editor, twenty minutes, no looking at these slides.

Draw it

Draw the five-stage flow from memory. Then write tally_votes and winners_of, and a test for each. Do not write main until both tests pass.

Anything you had to look up is the agenda for next session. Bring the file.

64. What you can do now

Recap

Six sections, and the fourth one is the one graders quietly test on every assignment of this shape.

inputexpected output
Alice, Bob, Alice, Dave, Charlie, AliceAlice 3, Bob 1, Charlie 1, one rejection
no votes at allevery candidate 0, all tied
a tally with two on 3both names, sorted, joined with 'and'

The Python Tutorial — python.org — every construct used in this deck

Sources

  1. The Python Tutorial — python.org
  2. Python Standard Library — Mapping Types (dict)
  3. Python Standard Library — collections.Counter
  4. PEP 8 — Style Guide for Python Code
  5. Python HOWTO — Sorting Techniques
  6. CS50x — Harvard's introduction to computer science, the plurality and runoff problem sets

Want this taught 1-on-1? Alexander tutors IT Support & Networking — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108