Python Multithreading
Python Multithreading Interview with follow-up questions
1. Can you explain what multithreading is in Python?
Multithreading runs multiple threads — independent flows of execution — inside one process, sharing the same memory. In Python you create them with the threading module.
import threading
def worker(name: str) -> None:
print(f"running {name}")
t = threading.Thread(target=worker, args=("A",))
t.start()
t.join()
The point interviewers always want is the GIL caveat, and in 2026 it's nuanced:
- Standard CPython has a Global Interpreter Lock, so in the default build only one thread executes Python bytecode at a time. Threads therefore give no speedup for CPU-bound work — for that, use
multiprocessing/concurrent.futures.ProcessPoolExecutor, or async. - Threads shine for I/O-bound work (network, disk, DB): the GIL is released during blocking I/O, so other threads run while one waits.
- The 2026 update: Python 3.13 introduced an experimental free-threaded (no-GIL) build; Python 3.14 (Oct 2025) made free-threading officially supported via PEP 779/703 — though it's still not the default build and carries a ~5–10% single-thread overhead. So "Python threads can't run in parallel" is now build-dependent, not absolute.
- Shared mutable state needs synchronization (
Lock,RLock, etc.); race conditions and deadlocks are the classic risks. For pools, preferThreadPoolExecutorover managing raw threads.
Follow-up 1
How does Python handle multithreading?
Python has a Global Interpreter Lock (GIL) which ensures that only one thread executes Python bytecode at a time. This means that even though multiple threads are created, they cannot fully utilize multiple CPU cores for parallel execution. However, Python provides a threading module which allows for concurrent execution of threads by switching between them at specific points. This can still be useful for I/O-bound tasks or tasks that involve waiting for external resources.
Follow-up 2
What is the Global Interpreter Lock (GIL) in Python?
The Global Interpreter Lock (GIL) is a mechanism used in CPython, the reference implementation of Python, to synchronize access to Python objects. It ensures that only one thread executes Python bytecode at a time, even on multi-core systems. This means that in a multi-threaded Python program, only one thread can be executing Python code at any given time, while other threads are blocked. The GIL is necessary to simplify memory management and avoid conflicts between threads accessing Python objects, but it can limit the performance benefits of multithreading in CPU-bound tasks.
Follow-up 3
Can you give an example of when you would use multithreading in Python?
Multithreading in Python can be useful in scenarios where the tasks are I/O-bound rather than CPU-bound. For example, if you have a web scraping application that needs to fetch data from multiple websites, you can use multithreading to perform the requests concurrently and improve the overall performance. Another example is a server application that needs to handle multiple client connections simultaneously. By using multithreading, each client connection can be handled in a separate thread, allowing for concurrent processing.
Follow-up 4
What are the advantages and disadvantages of multithreading?
Advantages of multithreading in Python include:
- Improved performance for I/O-bound tasks
- Concurrent execution of multiple tasks
- Simplified programming model for certain scenarios
Disadvantages of multithreading in Python include:
- Limited performance benefits for CPU-bound tasks due to the Global Interpreter Lock (GIL)
- Increased complexity and potential for race conditions and synchronization issues
- Difficulty in debugging and profiling multithreaded programs
It's important to carefully consider the specific requirements and characteristics of your application before deciding to use multithreading.
2. How do you create a thread in Python?
You create a thread with the threading module — either by passing a target callable or by subclassing Thread. start() launches it; join() waits for it to finish.
import threading
def worker(name: str) -> None:
print(f"running {name}")
t = threading.Thread(target=worker, args=("A",))
t.start() # runs worker() concurrently
t.join() # block until it completes
What interviewers expect beyond the snippet:
- Prefer a pool over raw threads.
ThreadPoolExecutormanages lifecycle and collects results for you:
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=4) as pool:
results = list(pool.map(fetch, urls)) # great for I/O-bound work
start()vsrun()— callstart()to launch a new thread; callingrun()directly just executes in the current thread (a common mistake).daemon=Truethreads don't block interpreter exit — useful for background tasks, but they're killed abruptly with no cleanup.args/kwargspass parameters; never share mutable state between threads without aLock.- Because of the GIL, this helps I/O-bound workloads; for CPU-bound parallelism use
ProcessPoolExecutor(or the free-threaded 3.14 build).
Follow-up 1
What is the difference between a thread and a process in Python?
In Python, a thread is a separate flow of execution within a process. Multiple threads can exist within a single process and share the same memory space. Threads are lightweight and are used for concurrent execution of tasks.
On the other hand, a process is an instance of a program that is being executed. Each process has its own memory space and resources. Processes are heavier compared to threads and are used for parallel execution of tasks.
Follow-up 2
How do you manage multiple threads in Python?
To manage multiple threads in Python, you can use synchronization mechanisms such as locks, semaphores, and condition variables to coordinate the execution of threads and prevent race conditions.
The threading module provides various synchronization primitives that can be used for managing multiple threads. For example, you can use the Lock class to create a lock object that can be acquired and released by threads to ensure exclusive access to a shared resource.
Here is an example of managing multiple threads using locks:
import threading
# Define a shared resource
shared_resource = 0
# Create a lock
lock = threading.Lock()
# Define a function that will be executed in the thread
def increment_shared_resource():
global shared_resource
with lock:
shared_resource += 1
# Create multiple threads
threads = []
for _ in range(10):
thread = threading.Thread(target=increment_shared_resource)
threads.append(thread)
# Start the threads
for thread in threads:
thread.start()
# Wait for all threads to finish
for thread in threads:
thread.join()
# Print the final value of the shared resource
print('Final value of shared resource:', shared_resource)
Follow-up 3
Can you give an example of creating and managing multiple threads in Python?
Sure! Here is an example of creating and managing multiple threads in Python:
import threading
# Define a shared resource
shared_resource = 0
# Create a lock
lock = threading.Lock()
# Define a function that will be executed in the thread
def increment_shared_resource():
global shared_resource
with lock:
shared_resource += 1
# Create multiple threads
threads = []
for _ in range(10):
thread = threading.Thread(target=increment_shared_resource)
threads.append(thread)
# Start the threads
for thread in threads:
thread.start()
# Wait for all threads to finish
for thread in threads:
thread.join()
# Print the final value of the shared resource
print('Final value of shared resource:', shared_resource)
3. What are the challenges of multithreading in Python?
The main challenges of multithreading in Python:
The GIL (in standard CPython). The Global Interpreter Lock lets only one thread run Python bytecode at a time in the default build, so threads give no parallel speedup for CPU-bound work — you need
multiprocessing/ProcessPoolExecutorfor that. The 2026 nuance: Python 3.14 officially supports a free-threaded (no-GIL) build (PEP 779/703), but it's not the default and adds ~5–10% single-thread overhead, so the GIL is still the norm most code runs under.Race conditions. When threads read/modify shared mutable state concurrently, results depend on timing. Even
counter += 1is non-atomic (load–add–store). Guard shared state withLock/RLock:
lock = threading.Lock()
with lock:
counter += 1
Deadlocks. Two threads each holding a lock the other needs hang forever. Acquire locks in a consistent global order, or use timeouts.
Non-deterministic bugs. Threaded defects depend on scheduling, so they're hard to reproduce, debug, and test. Prefer high-level tools —
queue.Queue(thread-safe) for producer/consumer,concurrent.futuresfor pools — over hand-rolled synchronization.
A strong answer adds: for I/O-bound concurrency, asyncio often beats threads (no locking, lower overhead); for CPU-bound work, processes or the free-threaded build are the real answer.
Follow-up 1
How does the Global Interpreter Lock (GIL) affect multithreading in Python?
The Global Interpreter Lock (GIL) is a mechanism in CPython that allows only one thread to execute Python bytecode at a time. This means that even if you have multiple threads in your Python program, only one thread can execute Python code at any given time. As a result, the GIL can limit the performance benefits of multithreading in CPU-bound tasks, as the threads are effectively serialized.
However, it's important to note that the GIL does not prevent the use of multiple threads in Python. It primarily affects CPU-bound tasks where the threads spend most of their time executing Python bytecode. For I/O-bound tasks, such as network requests or file operations, the GIL is released, allowing other threads to execute concurrently and potentially improve performance.
Follow-up 2
What are some ways to overcome these challenges?
There are several ways to overcome the challenges of multithreading in Python:
Use multiprocessing: Instead of using threads, you can use the multiprocessing module, which allows you to create multiple processes that can run in parallel. Each process has its own Python interpreter and memory space, so the GIL is not a limitation. However, inter-process communication can be more complex than inter-thread communication.
Use asynchronous programming: Instead of using threads or processes, you can use asynchronous programming frameworks like asyncio or Twisted. These frameworks allow you to write non-blocking code that can handle multiple I/O-bound tasks concurrently without the need for threads or processes.
Use thread-safe data structures: Instead of manually synchronizing access to shared data using locks, you can use thread-safe data structures provided by the threading module, such as Queue or deque. These data structures are designed to be used in multithreaded environments and handle synchronization internally.
Use thread pools: Instead of creating and managing threads manually, you can use thread pools provided by the concurrent.futures module. Thread pools allow you to submit tasks to a pool of worker threads, which can help manage the number of threads and simplify thread synchronization.
Use fine-grained locking: Instead of using a single lock to protect an entire data structure, you can use fine-grained locking techniques like read-write locks or lock striping. These techniques allow multiple threads to read from a data structure concurrently, while still ensuring exclusive access during writes.
Use profiling and optimization: If you have CPU-bound tasks that are heavily affected by the GIL, you can use profiling tools like cProfile to identify performance bottlenecks and optimize the code. This may involve using native extensions or moving computationally intensive tasks to separate processes.
Follow-up 3
Can you give an example of a problem you've encountered with multithreading and how you solved it?
Sure! One problem I encountered with multithreading was a race condition in a producer-consumer scenario. I had multiple producer threads that were producing items and putting them into a shared queue, and multiple consumer threads that were consuming items from the queue.
The problem was that sometimes the consumer threads would try to consume an item from the queue before any items were produced, resulting in an empty queue and causing the consumer threads to block indefinitely.
To solve this problem, I used a synchronization mechanism called a condition variable. The producer threads would acquire a lock, check if the queue was full, and if so, wait on the condition variable. When a consumer thread consumed an item and the queue became non-empty, it would notify the waiting producer threads using the condition variable.
By using the condition variable, I was able to ensure that the consumer threads would only consume items from the queue when there were actually items available, preventing them from blocking on an empty queue.
4. How can you synchronize threads in Python?
You synchronize threads with primitives from the threading module that coordinate access to shared resources and prevent race conditions:
Lock— the basic mutex; only one thread holds it at a time. Use it as a context manager so it's always released:
lock = threading.Lock()
with lock:
shared_counter += 1
RLock— reentrant lock the same thread can acquire multiple times (needed for recursive or nested locking).Semaphore— allows up to N concurrent holders; good for limiting access to a pool of resources.Event— a simple flag one thread sets and others wait on (wait()/set()), for signaling.Condition— wait/notify on a shared predicate, the basis of producer/consumer coordination.Barrier— blocks threads until a fixed number have arrived, then releases them together.
What interviewers want you to add:
- Prefer
queue.Queuefor producer/consumer — it's thread-safe internally, so you avoid manual locking entirely. - Always release locks — use
withrather thanacquire()/release()to stay exception-safe. - Lock ordering matters: acquire multiple locks in a consistent order to avoid deadlocks, and keep critical sections small to reduce contention.
Follow-up 1
What are some tools or techniques for synchronizing threads in Python?
Some commonly used tools or techniques for synchronizing threads in Python are:
Locks: Locks are the simplest synchronization primitive in Python. They allow only one thread to acquire the lock at a time, ensuring exclusive access to a shared resource.
Semaphores: Semaphores are used to control access to a shared resource by limiting the number of threads that can access it simultaneously.
Condition Variables: Condition variables allow threads to wait for a certain condition to become true before proceeding. They are often used in producer-consumer scenarios.
Barriers: Barriers are synchronization primitives that allow a group of threads to wait for each other at a certain point before proceeding further.
Follow-up 2
Can you give an example of synchronizing threads in Python?
Sure! Here's an example of using a Lock to synchronize threads in Python:
import threading
# Shared resource
counter = 0
# Lock object
lock = threading.Lock()
# Function to increment the counter
def increment_counter():
global counter
with lock:
counter += 1
# Create multiple threads
threads = []
for _ in range(10):
thread = threading.Thread(target=increment_counter)
threads.append(thread)
thread.start()
# Wait for all threads to finish
for thread in threads:
thread.join()
# Print the final value of the counter
print('Counter:', counter)
Follow-up 3
What are the potential issues if threads are not properly synchronized?
If threads are not properly synchronized, several issues can arise, including:
Race conditions: Race conditions occur when multiple threads access and modify shared data concurrently, leading to unpredictable and incorrect results.
Data corruption: Without proper synchronization, threads can interfere with each other's operations, leading to data corruption or inconsistent state.
Deadlocks: Deadlocks can occur when two or more threads are waiting for each other to release resources, resulting in a situation where none of the threads can proceed.
Starvation: Starvation happens when a thread is unable to acquire the necessary resources due to other threads constantly holding them, leading to the thread being unable to make progress.
Proper synchronization is essential to avoid these issues and ensure the correct and predictable behavior of multi-threaded programs.
5. What is thread safety and how is it achieved in Python?
Thread safety means code behaves correctly when multiple threads run it concurrently, with no corrupted state or race conditions. You achieve it by controlling access to shared mutable state.
Main techniques:
- Synchronization primitives. Wrap critical sections in a
Lock/RLockso only one thread mutates shared data at a time:
lock = threading.Lock()
with lock:
balance -= amount
- Thread-safe data structures.
queue.Queueis internally synchronized — using it for hand-offs lets you avoid explicit locks. - Avoid shared mutable state. Give each thread its own data (
threading.local()), use immutable objects, or pass results back through a queue rather than sharing variables.
The GIL nuance interviewers want corrected: the GIL does not give you thread safety. It makes individual bytecode ops atomic, but a compound operation like counter += 1 spans several bytecodes and can still interleave — so you still need locks. And in the free-threaded build officially supported in Python 3.14 (PEP 779), there's no GIL at all, making explicit synchronization even more essential. Treat the GIL as an implementation detail, never as a correctness guarantee.
Follow-up 1
What are some common thread safety issues in Python?
Some common thread safety issues in Python include:
Race conditions: These occur when multiple threads access and modify shared data concurrently, leading to unexpected and incorrect results.
Deadlocks: These occur when two or more threads are blocked indefinitely, waiting for each other to release resources.
Data corruption: This can happen when multiple threads modify shared data simultaneously without proper synchronization, leading to inconsistent or corrupted data.
Inconsistent state: This occurs when a thread reads shared data while another thread is in the process of modifying it, resulting in an inconsistent or incorrect state.
To avoid these issues, it is important to use proper synchronization techniques and thread-safe data structures when working with shared resources in Python.
Follow-up 2
Can you give an example of a thread-safe and a non-thread-safe operation in Python?
Sure! Here are examples of a thread-safe and a non-thread-safe operation in Python:
- Thread-safe operation:
from threading import Lock
counter = 0
lock = Lock()
# Thread-safe increment operation
def increment():
global counter
with lock:
counter += 1
# Create multiple threads to increment the counter
threads = []
for _ in range(10):
thread = Thread(target=increment)
thread.start()
threads.append(thread)
# Wait for all threads to finish
for thread in threads:
thread.join()
print(counter) # Output: 10
In this example, a lock is used to ensure that only one thread can modify the counter variable at a time, preventing race conditions and ensuring that the final value of counter is correct.
- Non-thread-safe operation:
from threading import Thread
counter = 0
# Non-thread-safe increment operation
def increment():
global counter
counter += 1
# Create multiple threads to increment the counter
threads = []
for _ in range(10):
thread = Thread(target=increment)
thread.start()
threads.append(thread)
# Wait for all threads to finish
for thread in threads:
thread.join()
print(counter) # Output: 7 or any other unexpected value
In this example, multiple threads are concurrently modifying the counter variable without any synchronization, leading to race conditions and incorrect results. The final value of counter can be unpredictable and may vary between different runs of the program.
Follow-up 3
What are some techniques for ensuring thread safety in Python?
There are several techniques for ensuring thread safety in Python:
Use thread-safe data structures: Python provides thread-safe data structures such as
Queue,Lock,Semaphore, andConditionin thethreadingmodule. These data structures can be used to coordinate access to shared resources and ensure that only one thread can modify them at a time.Use synchronization primitives: Synchronization primitives like locks, semaphores, and condition variables can be used to protect critical sections of code and ensure that only one thread can execute them at a time. This helps prevent race conditions and ensures thread safety.
Avoid shared mutable state: Shared mutable state can lead to race conditions and other thread safety issues. To ensure thread safety, it is recommended to avoid shared mutable state as much as possible. Instead, use immutable data structures or thread-local storage.
Use atomic operations: Atomic operations are operations that are guaranteed to be executed as a single, indivisible unit. Python provides atomic operations such as incrementing or decrementing an integer variable, which can be used to ensure thread safety without the need for explicit synchronization.
By applying these techniques, you can ensure thread safety in your Python programs and avoid common thread safety issues.
6. What is the GIL, and is it being removed in modern Python?
The single most-asked Python concurrency question in 2026. The Global Interpreter Lock (GIL) is a mutex in CPython that lets only one thread execute Python bytecode at a time, even on a multi-core machine. It exists to make memory management (reference counting) thread-safe and to keep the C internals simple.
Consequences you must be able to state:
- CPU-bound multithreaded code does not speed up with threads — the GIL serializes them. Use
multiprocessing/concurrent.futures.ProcessPoolExecutor(separate processes, separate GILs) for parallel CPU work. - I/O-bound code does benefit from threads, because the GIL is released during blocking I/O (and
asynciois the modern high-concurrency alternative). - The GIL does not make your code automatically thread-safe —
counter += 1is still a non-atomic read-modify-write and needs aLock.
The 2026 update interviewers want to hear: PEP 703 free-threading shipped as an experimental build in Python 3.13 and, via PEP 779, became officially supported (but not the default) in Python 3.14. A free-threaded build runs without the GIL using per-object locking and biased reference counting, giving true thread parallelism — at a ~5–10% single-thread overhead, and C extensions must be rebuilt for the new ABI. So "Python can never run threads in parallel" is now outdated; the nuance is "not in the standard GIL build, but free-threaded builds exist."
Live mock interview
Mock interview: Python Multithreading
- Read your scene and goals
- Talk it out; goals tick off live
- Get a score and stronger lines
Your voice and your AI key never touch our servers; the key stays in this browser and is sent only to Google. Only your round scores are saved to track progress.