A solved Python exercise on dictionaries, sets and tuples: workshop sign-ups, duplicates, intersection and every edge case explained step by step.
Updated on
Exam Code Python Data structures Basic level Tested code

The student union runs weekend workshops and has saved the sign-ups in a plain text file, one per line. Every valid line contains exactly two words separated by whitespace: the workshop name and the student id, for example python ana. Names are case sensitive, so Ana and ana are two different students.
The file is not clean. Some lines are empty or made only of spaces, and those are discarded without being counted. Some lines have one, three or more words; those are ignored lines and they are counted. And some sign-ups are repeated: if the pair (workshop, student) already appeared in an earlier valid line, the new line is a duplicate, it is counted and it adds nothing else.
Write a Python program that reads every line from standard input until end of file and writes to standard output, in this order: one line workshop: n per workshop, in the order of first appearance, where n is the number of distinct students enrolled; then a line ALL: followed by the students enrolled in every workshop, in alphabetical order and separated by a comma and a space, or followed by a dash - if there are none; then DUPLICATES: d; and finally IGNORED: k.
If the input is empty or contains no valid line, no workshop line is printed and the common-students line shows the dash. You must use a dictionary to group by workshop, sets for the students and tuples to represent each sign-up. No external libraries are allowed.
Sample input
python ana
python luis
sql ana
python ana
networks ana
sql luis
networks marta
this line is malformed
sql anaExpected output
python: 2
sql: 2
networks: 2
ALL: ana
DUPLICATES: 2
IGNORED: 1- Reading the input: skips blank lines, counts as ignored any line that does not have exactly two words, and detects repeated sign-ups with a set of tuples
- 0.75
- Dictionary of sets mapping each workshop to its distinct students, preserving first-appearance order
- 0.75
- Correct computation of the students common to all workshops, without mutating the stored data and with the empty cases handled
- 0.75
- Exact output format: order of the lines, separators and the dash when there are no common students
- 0.25
Hints
Hint 1 · How do I know whether I have already seen a sign-up?
Represent each sign-up as a tuple (workshop, student) and store it in a set. Tuples of strings are immutable and therefore hashable: they can be elements of a set. Testing whether a tuple is already in the set costs, on average, constant time.
Hint 2 · Which structure groups students by workshop?
A dictionary whose key is the workshop and whose value is a set of students. Since Python 3.7 dictionaries preserve insertion order, so iterating over items() already gives the workshops in first-appearance order; you do not need an auxiliary list.
Hint 3 · How do I get the students present in every workshop?
It is the intersection of all the sets in the dictionary. Start from a copy of the first one and intersect with the rest using &=. Decide beforehand what to return when the dictionary is empty, and think about what happens if you forget the copy.
Solution
Explained solution
The solution splits the work into two pure functions and a main that only reads and writes. read_records walks the lines, filters them, detects duplicates and builds the dictionary of sets. common_to_all computes the intersection. That way each part can be tested on its own and every rubric row maps to one concrete function.
import sys
def read_records(lines):
"""Return (attendees, duplicates, ignored).
attendees: dict workshop -> set of students, in first-appearance order.
"""
records = set() # (workshop, student) tuples already seen
attendees = {}
duplicates = 0
ignored = 0
for line in lines:
parts = line.split()
if not parts:
continue # blank line: not counted
if len(parts) != 2:
ignored += 1
continue
pair = (parts[0], parts[1])
if pair in records:
duplicates += 1
continue
records.add(pair)
workshop, student = pair
if workshop not in attendees:
attendees[workshop] = set()
attendees[workshop].add(student)
return attendees, duplicates, ignored
def common_to_all(attendees):
"""Students present in every workshop (empty set if there are none)."""
if not attendees:
return set()
groups = list(attendees.values())
common = set(groups[0]) # copy: never touch the original
for students in groups[1:]:
common &= students
return common
def main():
lines = sys.stdin.read().splitlines()
attendees, duplicates, ignored = read_records(lines)
for workshop, students in attendees.items():
print(f"{workshop}: {len(students)}")
common = common_to_all(attendees)
if common:
print("ALL: " + ", ".join(sorted(common)))
else:
print("ALL: -")
print(f"DUPLICATES: {duplicates}")
print(f"IGNORED: {ignored}")
if __name__ == "__main__":
main()main reads the whole input with sys.stdin.read().splitlines(). That gives a list of lines without the trailing newline and behaves the same whether or not the last line ends in \n. With empty input the list is empty too, and the rest of the program needs no special case.
Inside read_records, line.split() with no arguments splits on any run of whitespace and drops leading and trailing blanks. That is why a line made only of spaces produces an empty list and is skipped with continue without being counted. If the list does not hold exactly two items, the line adds one to ignored. In the test "empty intersection and invalid lines", onlyOneWord and three words here give IGNORED: 2, while the blank line is not counted.
Each valid line becomes the tuple pair. The set records remembers every tuple already seen. If pair is there, duplicates grows and nothing else happens. In the example, the pair python ana appears twice and the pair sql ana appears twice as well, which gives exactly two repetitions and DUPLICATES: 2. Using a tuple and not a list is mandatory: a list is mutable, it is not hashable and it cannot live inside a set.
New sign-ups are added to attendees. If the workshop does not exist yet, its empty set is created and, since the dictionary preserves insertion order, that workshop stays in the position of its first appearance. In the example the output lists python, sql and networks in that order, each with 2 distinct students. In the test with three workshops sharing the same students, the order c, a, b is respected even though it is not alphabetical.
One nuance: the duplicate check through records is needed in order to count duplicates. For the head count it would not be necessary, because adding the same student twice to a set does not change its size. These are two different responsibilities and it is better not to mix them.
common_to_all returns an empty set when there are no workshops. Otherwise it copies the first set with set(groups[0]) and intersects in place with the rest. In the example, python holds {ana, luis} and sql holds {ana, luis}, so the intersection is still {ana, luis}; intersecting with networks, which holds {ana, marta}, leaves {ana}. In the empty-intersection test, {ana, luis} against {marta} yields the empty set and ALL: - is printed. With a single workshop the loop never runs and the common students are its own, as in the bash pedro case.
Finally, sorted(common) turns the set, which has no order, into a sorted list, and ", ".join glues it with the required separator. That is why the three-workshop test prints ALL: x, y, z even though the students signed up in a different order.
Test cases
| Case | Input | Expected output | Actual output | Result |
|---|---|---|---|---|
| statement example | python ana
python luis
sql ana
python ana
networks ana
sql luis
networks marta
this line is malformed
sql ana | python: 2
sql: 2
networks: 2
ALL: ana
DUPLICATES: 2
IGNORED: 1 | python: 2
sql: 2
networks: 2
ALL: ana
DUPLICATES: 2
IGNORED: 1 | OK |
| empty input | (empty) | ALL: -
DUPLICATES: 0
IGNORED: 0 | ALL: -
DUPLICATES: 0
IGNORED: 0 | OK |
| one workshop with one student | bash pedro | bash: 1
ALL: pedro
DUPLICATES: 0
IGNORED: 0 | bash: 1
ALL: pedro
DUPLICATES: 0
IGNORED: 0 | OK |
| empty intersection and invalid lines | one ana
one luis
two marta
onlyOneWord
three words here
two marta | one: 2
two: 1
ALL: -
DUPLICATES: 1
IGNORED: 2 | one: 2
two: 1
ALL: -
DUPLICATES: 1
IGNORED: 2 | OK |
| three workshops with the same students | c x
a z
c y
a y
c z
a x
b z
b x
b y | c: 3
a: 3
b: 3
ALL: x, y, z
DUPLICATES: 0
IGNORED: 0 | c: 3
a: 3
b: 3
ALL: x, y, z
DUPLICATES: 0
IGNORED: 0 | OK |
Actual outputs: code compiled with Python 3.13.16 and run in an isolated container on 9 November 2026.
Complexity
Let L be the number of lines and T the number of workshops. For each line the reader performs a split proportional to the line length plus a fixed number of set and dictionary operations, each of average constant cost. So read_records runs in O(L) on average if we treat the line length as bounded.
The intersection costs, in the worst case, the sum of the sizes of the sets involved, which is bounded by the number of distinct sign-ups: O(L). Sorting the common students costs O(c log c), where c is their number, and c never exceeds the size of the smallest set. In memory we store each distinct sign-up twice, once in records and once inside attendees, plus the list of lines: O(L) in total.
Common mistakes
- Writing
common = groups[0]without copying:&=mutates the set in place and destroys the students of the first workshop inside the dictionary. Any later query on that workshop returns a wrong result. - Trying to put lists into the set of sign-ups (
records.add([workshop, student])): Python raisesTypeError: unhashable type: 'list'. Tuples are the right choice. - Counting blank lines as ignored, or using
line.split(' '), which on double spaces produces empty strings and makes a correct line look like it has more than two words. - Initialising the common students with an empty set and intersecting with everything: the result is always empty. The intersection must start from the first set, not from the empty set.
- Printing the set directly (
print(common)) or throughstr: you get braces and quotes, and the order is not guaranteed. You must sort and join with the required separator. - Sorting the workshops alphabetically when printing them, while the statement demands first-appearance order.
Variants
Workshops per student: inverting the dictionary
If you are asked how many distinct workshops each student attended, in alphabetical order of student, just build the inverse dictionary from attendees: start with by_student = {} and, for every workshop, students pair and every s in students, call by_student.setdefault(s, set()).add(workshop). Then iterate over sorted(by_student) and print len(by_student[s]). Since a student may attend several workshops, the value must be a set or a counter, never a single overwritten key.
Students who attended exactly one workshop
You are asked for the set of students appearing in exactly one workshop. A clean way is to count occurrences with collections.Counter over the concatenation of all the sets (each student counts once per workshop) and keep those with count 1. It can also be done with plain sets: accumulate the students already seen in seen and those seen at least twice in repeated; the answer is seen - repeated.
The intersection in a single expression
The examiner may ask you to rewrite common_to_all without an explicit loop: return set.intersection(*attendees.values()) if attendees else set(). Calling set.intersection on the class passes the first set as self and returns a brand new set, so there is no aliasing problem. The guard is essential, because the call fails with no arguments.
On the syllabus
Grouping data by key with constant-time lookups is, underneath, a hash table, and hash tables appear in the syllabus of 6.006 Introduction to Algorithms at MIT, alongside search trees, sorting and graph search. At Carnegie Mellon, 15-122 Principles of Imperative Computation lists arrays, structs and generic data structures, the machinery you would have to write by hand if the language did not hand you a dict and a set. Seeing both sides makes the Python version far less magical.
CS107 Computer Organization and Systems at Stanford spends weeks on pointers, the heap and generic containers built with void *, which is exactly the cost that a Python set hides behind one line. Outside the English-speaking world, the syllabus of Estructuras de Datos at UNAM's Facultad de Ciencias covers arrays, lists and search trees, the structures used to implement dictionaries and sets when the language does not provide them. If you want more practice, try the stack and recursive expression evaluator in Python.
Article generated with AI.larebelion



Publicar un comentario