Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
201: Threading Basics with the threading Module
I want to talk about a moment most Python developers hit: the realization that your code is spending 90% of its time doing absolutely nothing. Specifically, I'm talking about I/O-bound tasks. If you're writing a script that calls five different APIs or downloads ten images, you've probably just written a loop that handles them one by one. It works, but it's painfully slow because your CPU is just sitting there, idling, waiting for a server halfway across the world to send a packet back.
The Cost of Waiting in Line
Let's look at the naive way to handle this. Imagine we have a list of URLs and a function that simulates a network request using time.sleep(). In a standard sequential script, it looks like this:
import time
def fetch_data(url):
print(f"Fetching {url}...")
time.sleep(2) # Simulating network latency
print(f"Done with {url}")
urls = ["api/users", "api/posts", "api/comments", "api/settings"]
start = time.time()
for url in urls:
fetch_data(url)
print(f"Total time: {time.time() - start:.2f} seconds")
If you run this, it'll take about 8 seconds. Why? Because the program is strictly linear. It fetches the users, waits two seconds, then moves to posts, waits two seconds, and so on. You're essentially standing in a single-file line at a coffee shop where only one person can order at a time, even though there are four baristas standing around doing nothing.
Breaking the Line with Threading
This is where the threading module comes in. Threading allows us to start a task and, instead of waiting for it to finish before moving to the next line of code, we tell Python: "Start this in the background and let me know when it's done."
Here is how I'd refactor that loop to actually utilize the downtime. I'll use a list to keep track of the threads so we can ensure the main program doesn't exit before the background work is finished:
import threading
import time
def fetch_data(url):
print(f"Fetching {url}...")
time.sleep(2)
print(f"Done with {url}")
urls = ["api/users", "api/posts", "api/comments", "api/settings"]
threads = []
start = time.time()
for url in urls:
# We create a Thread object, telling it which function to run and what arguments to use
thread = threading.Thread(target=fetch_data, args=(url,))
threads.append(thread)
thread.start()
# This is the crucial part: join() tells the main program to wait for the thread to finish
for thread in threads:
thread.join()
print(f"Total time: {time.time() - start:.2f} seconds")
Now, the total time drops to about 2 seconds. We've essentially opened four different checkout lanes at that coffee shop. The CPU sends the request for the first URL, and while it's waiting for the response, it immediately jumps to start the second thread, then the third, and so on.
When Threading Hits a Wall
Now, I need to give you a fair warning: threading in Python isn't a magic "make it fast" button for everything. You've probably heard of the GIL—the Global Interpreter Lock. In simple terms, the GIL ensures that only one thread executes Python bytecode at a time.
If your task is CPU-bound—like calculating a billion digits of Pi or processing a massive image—threading won't help you. In fact, it might actually slow you down because of the overhead involved in switching between threads. Threading is for waiting. If your code is waiting on a network, a disk read, or a user input, use threading. If your code is crunching numbers, you'll need to look into the multiprocessing module, which we'll cover later.
One last thing: managing a list of threads manually, like I did above, gets messy fast. If you're dealing with dozens or hundreds of tasks, you don't want to spawn 100 individual threads; you'll exhaust your system resources. In a professional production environment, I'd suggest using concurrent.futures.ThreadPoolExecutor, which manages a "pool" of threads for you. But before you jump to the high-level abstractions, you need to understand the underlying threading.Thread logic I've shown here.
📋 Practical Task
Build a Parallel Website Status Checker
Your task is to create a script that checks the availability of multiple websites simultaneously. Instead of using a real network library like requests (to keep this focused on threading logic), you will simulate the check using time.sleep().
- Create a list of at least 6 dummy website URLs.
- Write a function
check_status(url)that prints "Checking [url]...", sleeps for a random duration between 1 and 3 seconds (userandom.uniform(1, 3)), and then prints "[url] is ONLINE". - Implement the threading logic to launch these checks in parallel.
- Ensure that the main program waits for all threads to complete using
.join()before printing a final message: "All websites have been checked." - Measure and print the total execution time to prove that it took significantly less time than if the checks were run sequentially.
There are no comments for now.