Besides what I already said in the comments, I think that the way to choose the best hyperonym can be spoiled. The synthesis you ended up with is not the lowest common hyperonym for all words, but only for two of them.
Let it stick to your example of "school and office supplies." For each word in the expression, you get several synsets. Therefore, the node_synsets variable will look something like this:
[[school_1, school_2], [office_1, office_2, office_3], [supply_1]]
In this example, there are 6 ways to combine each synchronism with any of the others:
[(school_1, office_1, supply_1), (school_1, office_2, supply_1), (school_1, office_3, supply_1), (school_2, office_1, supply_1), (school_2, office_2, supply_1), (school_2, office_3, supply_1)]
These triples are what you repeat in the outer for loop (with itertools.product ). If the expression has 4 words, you will iterate over the fours, with 5 of its five, etc.
Now that the inner for loop, you join each triple. The first one is:
[(school_1, office_1), (school_1, supply_1), (office_1, supply_1)]
... and you define the lowest hyperonym among each pair. Therefore, in the end, you get the lowest hyperonym, say, school_2 and office_1 , which can be a kind of institution. This is probably not very significant, since it does not take into account any synchronization of the last word.
Perhaps you should try to find the smallest common hyperonym of all three words in each combination of their syntaxes and take one of them, the best among them.