When do you use threads and when do you use processes in Python?
The decision comes down to what your work is waiting on.
Use threads when the work is I/O-bound: network calls, database queries, file reads, waiting on a lock or a subprocess. Threads share memory, so passing results between them is free, and the interpreter switches between them whenever one blocks. A thousand threads waiting on HTTP responses is a perfectly reasonable design.
Use processes when the work is CPU-bound: number crunching, image processing, compression, heavy parsing. Processes have separate memory and therefore separate interpreters, so they run on separate cores with no GIL contention. The cost is that they cannot share ordinary Python objects — state has to be sent through a multiprocessing.Queue or a Pipe, and everything crossing that boundary has to be picklable.
There is a third option worth knowing. The concurrent.futures module gives you both under one interface: ThreadPoolExecutor for I/O and ProcessPoolExecutor for CPU work. The executor API is the same for both, so switching is a one-line change, and futures give you a clean way to submit work, wait for a result and handle a failure.
Two practical points for an interview. The overhead of a process is much higher than a thread, because starting a process means starting a whole interpreter and copying or re-creating the state. And in Python 3.8 and later, the default start method on Linux changed to fork, which is cheap but can be unsafe in a program that has threads; the forkserver and spawn methods are the safer choices when you care.