Lesson 2 · Beginner · 20 min
Check an official receipt series for gaps and duplicates
Compare the receipts recorded today with the booklet issued, and flag missing, repeated or stray numbers.
✓ Last checked: November 2026 · code run, output real, facts checked
In this lesson
- Use a set to compare what was expected with what was recorded
- Find repeated values with collections.Counter
- Write short filters with list and set comprehensions
Official receipts are accountable forms. Every number in a booklet must be accounted for: used, cancelled, or still unused at the end. A skipped number or a number entered twice is the kind of thing an auditor will ask about, and it is much easier to explain on the same day than months later. This lesson checks a day's receipts against the booklet issued to the collector.
The data
Save this as receipts.csv. The booklet issued runs from 7650101 to 7650150, and a few problems have been planted in the list on purpose.
or_number,status,amount
7650101,Issued,1250.00
7650102,Issued,300.00
7650103,Issued,4500.00
7650104,Issued,105.00
7650105,Issued,980.40
7650107,Issued,600.00
7650108,Issued,2350.50
7650108,Issued,1800.00
7650109,Issued,75.00
7650110,Issued,12750.00
7650112,Issued,1250.00
7650113,Cancelled,0.00
7650114,Issued,4500.00
7650115,Issued,105.00
7650116,Issued,980.40
7650117,Issued,600.00
7650118,Issued,2350.50
7650119,Issued,1800.00
7650120,Issued,75.00
7650151,Issued,12750.00
The code
"""Check the official receipts used today against the booklet issued to the collector."""
import csv
from collections import Counter
BOOKLET_FIRST = 7650101 # Accountable Form No. 51, one booklet of 50 receipts
BOOKLET_LAST = 7650150
with open("receipts.csv", newline="", encoding="utf-8") as f:
rows = list(csv.DictReader(f))
numbers = [int(row["or_number"]) for row in rows]
cancelled = [int(row["or_number"]) for row in rows if row["status"] == "Cancelled"]
# 1. Numbers that appear more than once
counts = Counter(numbers)
duplicates = sorted(n for n, times in counts.items() if times > 1)
# 2. Numbers that do not belong to the booklet
outside = sorted(n for n in set(numbers) if not BOOKLET_FIRST <= n <= BOOKLET_LAST)
# 3. Gaps: every number from the first to the last one used should be accounted for
inside = {n for n in numbers if BOOKLET_FIRST <= n <= BOOKLET_LAST}
expected = set(range(min(inside), max(inside) + 1))
gaps = sorted(expected - inside)
print(f"Receipts recorded: {len(numbers)}")
print(f"Series used: {min(inside)} to {max(inside)}")
print(f"Cancelled: {cancelled or 'none'}")
print(f"Duplicates: {duplicates or 'none'}")
print(f"Outside booklet: {outside or 'none'}")
print(f"Missing numbers: {gaps or 'none'}")
if duplicates or outside or gaps:
print("\nStatus: NOT BALANCED. Explain each item above before preparing the report.")
else:
print("\nStatus: series is complete.")
Output
Receipts recorded: 20 Series used: 7650101 to 7650120 Cancelled: [7650113] Duplicates: [7650108] Outside booklet: [7650151] Missing numbers: [7650106, 7650111] Status: NOT BALANCED. Explain each item above before preparing the report.
How it works
Counter(numbers) counts how many times each receipt number appears. Any number with a count above one was recorded twice.
BOOKLET_FIRST <= n <= BOOKLET_LAST Python lets you chain comparisons, so this reads just like the rule: the number must fall inside the booklet.
expected - inside subtracting one set from another leaves the numbers that should be there but are not. That single line finds every gap.
duplicates or 'none' an empty list counts as false in Python, so the report prints none instead of [].
Note: A cancelled receipt is not a gap. It still uses up its number, so it stays in the list and is reported on its own line.
Try it yourself
Practice
Change the script so it also prints how many receipts remain unused in the booklet, from the last number used up to 7650150.
Show answer
"""Answer: how many receipts are still unused in the booklet."""
import csv
BOOKLET_FIRST = 7650101
BOOKLET_LAST = 7650150
with open("receipts.csv", newline="", encoding="utf-8") as f:
numbers = [int(row["or_number"]) for row in csv.DictReader(f)]
inside = [n for n in numbers if BOOKLET_FIRST <= n <= BOOKLET_LAST]
last_used = max(inside)
unused = BOOKLET_LAST - last_used # numbers after the last one used
print(f"Last receipt used: {last_used}")
print(f"Unused receipts: {unused} ({last_used + 1} to {BOOKLET_LAST})")
Last receipt used: 7650120 Unused receipts: 30 (7650121 to 7650150)