Seeing as how it works in all of these cases, am I missing something obvious - should I be using a resistor or not?
The one obvious point that you are missing is that the two GPIO's normally used for I2C already have (1.8K) pull-up resistors to 3V3, all other GPIO's need pullups.
furthermore, for safety purposes there actually should also be a 1K current limiting resistor in-between the GPIO, and the node between the switch and pull-up if you are using your own pull-up, (but for the on-board pullups this isn't possible).
The reason is that you might make a mistake and program the GPIO used for the button as an output, and program it to output 3.3V, if you then push the button you short the 3.3V to GND and the short-circuit current can then damage the GPIO. the 1K resistor limits the current to a safe value, while doing nothing for its normal input function.
Lastly, if you connect a 100nF capacitor over the switch you wont be bothered (when reading the switch) by bouncing contacts of a closing switch. The CPU is so fast it can "see" multiple key presses otherwise, unless you write software to suppress key bounce.
finally, the PI's GPIO logic is quite fancy, and its possible to program an internal (to the PI's chip) pull-up (or pull-down). You may think this makes an external pull-up unnecessary, but this isn't completely true as the pull-up is relatively weak (probably around 50K), and long wires may pick up (as an antenna) electrical noise which may override the weak pull-up so use an external pull-up anyway, a good value being 4K7 or 10K.