Navigating Challenging Python Interview Questions: Part I
Written on
Chapter 1: Introduction to Python Interview Challenges
In my journey as a programmer, I have encountered various challenging interview questions. This article aims to highlight three particularly tough questions that I have faced during Python-related interviews. While these might not be the absolute hardest, they certainly rank high on many candidates' lists. By discussing these questions, I hope to provide you with insights and explanations to better prepare you for your own interviews.
This is the first part of a two-part series, where I will present three questions here, followed by three more in the next installment. My goal is to offer a solid understanding of these topics rather than an exhaustive exploration.
Section 1.1: Understanding the Global Interpreter Lock (GIL)
The Global Interpreter Lock (GIL) is a concept I was asked about a few years ago, and I was not prepared for it at all. Although I had heard of GIL before, I had never taken the time to delve into its intricacies.
To understand GIL, we first need to grasp the concepts of threads and processes.
Process
In the realm of Python, a process can be viewed as an instance of a running Python application. Each process operates independently, possessing its own memory, environment, CPU time, and resources. This means that if you run multiple Python scripts on your machine, they function in isolation from one another. The concept of a process is crucial, and we will revisit it later with practical examples.
Thread
To simplify the explanation, let’s use a real-world analogy involving a restaurant and a chef. Imagine you are a chef working in a kitchen, which serves as your "office." This kitchen is where all the vital actions occur. We can liken the kitchen to a process, where all necessary ingredients and tools are kept.
However, to cook, we need a chef, who represents our thread. The chef accesses the ingredients (memory) and operates the tools (CPU). In Python, when a program is initiated, it starts with a single thread, known as the main thread. Python allows for the creation of multiple threads within a single process, which can share the same memory, much like having several chefs in a restaurant managing various orders.
Now, Let's Dive into GIL
When a Python process is initiated, it always begins with a single thread—the main thread. The GIL is a mutex that ensures that only one thread can execute Python bytecode at any given moment. The primary purpose of GIL is to simplify memory management and guarantee thread safety. Since threads within the same process share memory, there exists a risk of race conditions.
A race condition occurs when two or more threads access shared data simultaneously. Depending on the system's design, if one thread modifies the shared data, it could lead to significant issues for the other thread.
# Thread A - Salary payment
salary = 4000
def pay_salary():
# process the payment
global salary
# Thread B - Read and modify salary's value
def update_salary():
global salary
salary = 5000
In this example, we have two threads—A and B. Thread A pays the salary based on the current value, while Thread B modifies the salary. If both threads execute concurrently and Thread B changes the salary before Thread A processes the payment, the salary paid could be incorrect.
The GIL presents a trade-off regarding concurrency. While it simplifies memory management, it also constrains performance for CPU-bound tasks and multithreading, which we will explore next.
Section 1.2: Distinguishing Multiprocessing from Multithreading
A common interview question is: "What is the difference between multiprocessing and multithreading?"
Multiprocessing
In essence, multiprocessing refers to the ability to run more than one process simultaneously. Each process comes with its own Python interpreter, memory space, and GIL. By default, a Python process uses only one CPU core, even on machines with multiple cores.
Utilizing multiprocessing allows us to leverage our machine's full potential and enhance performance by executing multiple processes across available cores. You can implement multiprocessing in Python using the built-in multiprocessing package.
import multiprocessing
# Using multiprocessing provides true parallelism
from multiprocessing import Pool
from dataclasses import dataclass
@dataclass
class MessageObject:
recipient: str
message: str
def send_message(msg_obj: MessageObject) -> None:
print(f"sending message to - {msg_obj.recipient}.")
# additional logic
if __name__ == '__main__':
with Pool(4) as pool:
pool.map(
send_message,
[
MessageObject(recipient="Foo", message="Bar"),
MessageObject(recipient="John", message="Doe")
]
)
In the code snippet above, we create four processes using the Pool object. Python will typically utilize the number of available CPUs unless specified otherwise. Here, we have two tasks, and the map method will automatically assign them to the available processes.
Multiprocessing is particularly advantageous for CPU-bound tasks requiring heavy computations.
Multithreading
By default, Python starts only one thread in a process to prevent race conditions. To use more than one thread within a single process, we turn to multithreading through the built-in threading module.
import threading
from dataclasses import dataclass
@dataclass
class MessageObject:
recipient: str
message: str
def send_message(msg_obj: MessageObject) -> None:
print(f"sending message to - {msg_obj.recipient}.")
# additional logic
if __name__ == '__main__':
foo_thread = threading.Thread(target=send_message, args=(MessageObject(recipient="Foo", message="Bar"),))
john_thread = threading.Thread(target=send_message, args=(MessageObject(recipient="John", message="Doe"),))
foo_thread.start()
john_thread.start()
foo_thread.join()
john_thread.join()
In this example, we create threads for each message we need to send and start them using the start method. The join method blocks the calling thread until the thread it is called on terminates.
Multithreading can be particularly useful for I/O-bound tasks, such as making API requests.
The first video titled "Solving Coding Interview Questions in Python on LeetCode (easy & medium problems)" provides practical solutions to common coding challenges, enhancing your problem-solving skills for interviews.
The second video "Python Coding Interview Practice - Difficulty: Hard" dives into more challenging questions, perfect for those looking to sharpen their coding interview abilities.
Section 1.3: Concurrency in Python
"Concurrency is about dealing with a lot of things at once." — Rob Pike
In Python, we can achieve concurrency through either multiprocessing or multithreading, or by employing asynchronous programming techniques.
Chapter 2: Conclusion and Learning Opportunities
While interviews can be daunting, they also present excellent opportunities for learning. Many of the questions I struggled with in the past have become valuable lessons. I hold onto notes from previous interviews that I find relevant for ongoing improvement. In the next article, I will compile three additional challenging questions to share with you.
About the Author
Yanick is a Solutions Engineer for a Spanish company, currently residing in Portugal. He has been coding in Python since 2018 and sharing insights on Medium since 2020. Yanick aims to provide valuable knowledge about Python and programming to help enhance your skills. Join him on this journey!