We mentioned previously that you have to set up USB devices in boot.py, not code.py.
boot.py runs before CircuitPython connects to the host computer via USB. When code.py runs, it's too late to change the USB devices. Here is what happens, step by step:
The board does a hard restart. This happens when you plug the board in, press the reset button, or do
microcontroller.reset() in your program.
boot.py runs, if it exists. The board does not yet present any USB devices to the host computer. You set up USB devices in boot.py. Anything you print or any errors reported will go to boot_out.txt, since there is no console connection yet.
After boot.py has run, CircuitPython creates the data needed to tell the host about all the USB devices. This binary data is packaged into a number of USB descriptors.
CircuitPython tells the host USB is ready.
The host enumerates all the USB devices by asking for and receiving the descriptors, and setting up connections to the devices. At the same time, code.py starts to run. If there is no code.py, CircuitPython just starts the REPL.
Various things can go wrong in the steps above. For instance, if you make a programming error like a typo in boot.py, the error will show up in the boot_out.txt file in CIRCUITPY. They will not be printed on the console, since there is no USB connection yet.
But your code could also try to create too many USB devices. That error is not detected until step 3. Instead, CircuitPython will go into safe mode, and will not run code.py. If you go into the REPL, you will see CircuitPython reporting the reason it is in safe mode, such as "USB devices need more endpoints than are available".