Loading...
Loading...
Python interview guide
Threads, processes, the GIL and async code. Every answer is short enough to say aloud, and every question links to the lesson that teaches it.
31 questionsLast updated
A process has its own memory and its own Python interpreter. Threads live inside one process and share its memory. That makes threads cheap to start and easy to share data with, but shared data needs locks. Processes are isolated, so a crash or a bad write in one does not touch another.
Lesson: Threads and Race ConditionsA race condition is a bug where the result depends on the order in which threads run. Example: two threads run balance += 1. Each reads the old value, adds one and writes back. If both read before either writes, one update is lost. The fix is to make the read-change-write one step with a lock.
counter += 1 atomic in Python?No. It compiles to several steps: load the value, add one, store it back. The interpreter can switch threads between those steps, so two threads can lose an update. Protect it with a lock. Some single operations such as list.append are safe in CPython, but you should not rely on that for compound updates.
Lock and RLock?A Lock can be taken by one holder, and a thread that asks again for a lock it already holds waits forever. An RLock (reentrant lock) can be taken again by the same thread, as long as it is released the same number of times. Use RLock when a locked function calls another locked function of the same object.
A deadlock is when threads wait on each other forever, for example thread A holds lock 1 and wants lock 2 while thread B holds lock 2 and wants lock 1. Avoid it by always taking locks in one fixed order, holding as few locks as possible, and using timeouts such as lock.acquire(timeout=1).
queue.Queue between threads?It is thread-safe: it locks inside, so producers and consumers can share it without extra locks. get() waits when the queue is empty and put() can wait when it is full, so it also gives you simple coordination. It is the standard way to pass work to a pool of worker threads.
A daemon thread does not keep the program alive. When only daemon threads are left, Python exits and stops them mid-work. Use them for background chores such as heartbeats, never for work that must finish or must clean up. A normal thread is waited for at exit.
Lesson: Threads and Race Conditionsif __name__ == "__main__" guard?With the spawn start method, used on Windows and macOS, every child imports your main module. Without the guard that import would start new children again, without end. The guard makes start-up code run only in the parent. The functions the children call stay outside the guard.
Lesson: Multiprocessing: Processes and Poolsfork copies the parent process, which is fast, but it copies locks and threads in odd states and can be unsafe. spawn starts a fresh interpreter and imports the module; it is safer and the default on Windows and macOS. forkserver asks a clean helper process to fork. Pick one with multiprocessing.set_start_method.
By default they do not. Data moves by copying: through Queue, Pipe, or arguments and return values, all of which are pickled. For shared state there are Value, Array and shared_memory, and a Manager that serves objects through a helper process. Copying is slower than sharing, so keep the messages small.
A process needs its own interpreter and memory, and data crossing the boundary is pickled and copied. Starting one can take tens of milliseconds, so each job must be heavy enough to justify it. A thread starts in well under a millisecond and shares memory directly.
Lesson: Multiprocessing: Processes and PoolsFor CPU-bound work, such as image processing, number crunching or parsing big files, where the GIL stops threads running Python code in parallel. Each process has its own GIL, so they use several cores. For waiting work, such as network calls, threads or asyncio are cheaper.
Lesson: Multiprocessing: Processes and PoolsAnything that cannot be pickled: lambdas, nested functions, open files, sockets, database connections and thread locks. Pass a module-level function and plain data, and open resources inside the worker.
Lesson: Multiprocessing: Processes and Poolsconcurrent.futures and why use it?It is a high-level module with ThreadPoolExecutor and ProcessPoolExecutor. You submit jobs and get Future objects back, so you do not manage threads, queues and joins yourself. Both executors share one API, so moving from threads to processes is a small change.
Lesson: concurrent.futures: Executors and FuturesA Future represents the result of work that may not be done yet. result() waits and returns the value or raises the job's exception. done() checks without waiting, cancel() stops a job that has not started, and add_done_callback runs a function when the job ends.
map and submit with as_completed?map returns results in the order of the inputs and is the simplest choice. submit plus as_completed gives results in the order jobs finish, so you can react to fast jobs first, and each future can be handled for its own error. With as_completed you must keep track of which input produced which result.
They do not crash the pool. The exception is stored in the Future and raised again when you call result() or exception(). With map, it is raised when you reach that item while iterating. If you never read the future, the error is lost.
Look at where the time goes. If jobs wait for network, disk or a database, use threads: waiting does not use the CPU and the GIL is released. If jobs spend their time computing in Python, use processes so each has its own GIL. Processes cost start-up time and need picklable work, so very small jobs are better on threads.
Lesson: concurrent.futures: Executors and Futuresmax_workers default to and how should you set it?For threads the default is min(32, cpu_count + 4). For processes it is the number of CPUs. For waiting work you can set threads higher, as many as the remote service allows. For CPU work do not go above the core count, because extra processes only add switching cost.
The Global Interpreter Lock is a mutex in CPython that lets only one thread execute Python bytecode at a time within a process. It exists because CPython's memory management, reference counting, is not thread-safe, and one big lock is a simple and fast fix for single-threaded code. It is a feature of CPython, not of the Python language.
Lesson: The GIL in PracticeYes, for I/O-bound work. A thread that waits on the network, disk or time.sleep releases the GIL, so other threads run and the waits overlap. They do not help CPU-bound pure Python code, because only one thread can run bytecode at a time. For that, use processes.
No. The GIL guards the interpreter's internals. A thread can still be paused between the steps of x += 1, so you can lose updates. You still need locks, queues or other safe designs for shared state.
Use multiprocessing or ProcessPoolExecutor, so each worker has its own interpreter and GIL. You can also use C extensions that release the GIL, such as NumPy, or run the heavy part in another language. Python 3.13 also has an experimental free-threaded build without the GIL.
Lesson: The GIL in PracticeAsk what limits the program. For CPU-bound work use processes, as each gets a core. For I/O-bound work with a moderate number of waits, threads are simple and need little change to ordinary code. For thousands of waits, asyncio is lightest because tasks are cheap, but every library in the path must be async-aware.
Lesson: The GIL in PracticeRemoving it makes single-threaded code slower, because Python then needs finer locks or atomic reference counts. It also breaks many C extensions that count on the GIL. Python 3.13 offers an opt-in free-threaded build as a careful step, but the normal build keeps the GIL.
Lesson: The GIL in Practicetime.sleep release the GIL?Yes. Blocking calls such as time.sleep, socket reads and file reads release the GIL while they wait. That is why a thread pool can make 50 waits finish in about the time of one.
Split the two parts. Downloading waits on the network, so a thread pool, or asyncio, overlaps the waits. Compression is CPU work, so a process pool uses several cores. I would feed finished downloads to the process pool and cap both pools, so memory and the server are not overloaded.
Lesson: Threads and Processes CheckpointBecause of the GIL. Only one thread runs Python bytecode at a time, so threads take turns, and the switching adds a little cost. Use processes, a C extension that releases the GIL, or a faster algorithm.
Lesson: Threads and Processes CheckpointIn threads, protect the update with a Lock, or better, let each job return its own count and add them in one place. In processes, memory is separate, so use return values, a Queue, or multiprocessing.Value with its lock. Returning values is usually simplest.
A deadlock is when two or more threads each hold a lock the other needs, so none can proceed. Prevent it by taking locks in one global order, by holding fewer locks for a shorter time, or by using lock timeouts and handling the failure.
Lesson: Threads and Processes CheckpointDo not depend on finishing order. Join all threads, collect results into a structure keyed by input, and sort or index before comparing or printing. Using executor.map, which keeps input order, also helps.
Each lesson has quizzes, practice and more interview questions on the same topic.
Open the first lesson