Login Sign Up
Python Multithreading
Chapter 37 🟡 Intermediate

Python Multithreading

Master the concept step by step with clear explanations, examples, and code you can run.

Advanced Python Multithreading: Beyond a Basics

Hello there! Welcome back to our Python journey, while

since you already know the basics of Python—like how to write loops create dictionaries. Build simple scripts—we are actually going to take a massive step forward today. We are going to look under the hood of Python’s concurrency model to grasp how your computer can seemingly do basically 100 things at the exact same time.

Take a deep breath, and let's dive right in!

The Big Problem: Freezing the Program

Into modern programming you'll just mostly need to fetch data from a web. For this, almost the entire Python community relies on the external library called requests; it is a true giant in the programming world; to give you an idea of a scale this library pulls in an unbelievable 300 million downloads every single week and is depended upon by over 4000000 repositories, and because it is designed to be "HTTP for Humans," it officially supports Python 3.10+ and even runs beautifully on alternative implementations like PyPy.

Developers everywhere use it to seamlessly link their Python programs with a variety of APIs databases, and external systems that use JSON of data representation.

But here is a massive real-world problem.

By default, when you call the standard blocking function like requests.get(), your Python program completely freezes and stops doing anything else until the server replies. If you need for fetch live user statistics than 100 different third-party API endpoints doing them one by one will take forever.

How do we send 100 requests at the exact same time without our program grinding towards a halt?

GIL: A Constant Balancing Act

To grasp a solution we have to talk about threads and something called the Global Interpreter Lock (GIL).

As a developer juggling multiple tasks and processes is constant balancing act much like parenting. You want your computer's CPU to juggle multiple operations simultaneously. In Python, you can use threads to run multiple tasks concurrently.

Though, Python has a unique architectural feature called GIL. Because for Python's Global Interpreter Lock threads can't achieve true parallelism for heavy CPU-bound tasks (like intense mathematical calculations or processing large images). The GIL essentially locks the Python interpreter so that only one thread can execute Python bytecode at a time.

But here is the secret: Threads are absolutely perfect for I/O-bound tasks where the program spends most of its time just waiting around. When your code is waiting for a network request to finish or a file to download, a GIL is released!

A Cutting-Edge Solution: Bridging Async with Threads

Standard courses stop on basic asyncio. But in a modern 2024-2025 development landscape we need to bridge standard blocking code with modern asynchronous architectures.

A recent December 2024 deep dive explains that when you define an asynchronous function using the async keyword Python creates the coroutine object behind the scenes; inside that coroutine, you use the await keyword to yield control back to the event loop instantly allowing the system to do probably other useful work while waiting. Standard application developers should typically use high-level asyncio functions like asyncio.run() to manage this execution;

but how do we put our blocking requests.get() inside this modern async loop?

According for a latest Python 3.14.6 documentation, developers can use a brilliant function called asyncio.to_thread(func, /, *args, **kwargs). This function takes your blocking code and asynchronously runs it in an entirely separate thread, while any arguments you supply are directly passed along to that function, keeping your main program lightning-fast and unblocked, while

here is visual map with how that looks under the hood:

sequenceDiagram
    participant EventLoop as Main Async Event Loop
    participant Thread as Separate OS Thread
    participant API as Third-Party Server

    EventLoop->>Thread: 1. Offload blocking requests.get() using asyncio.to_thread()
    Note over EventLoop: 2. Event loop is FREE to run other tasks!
    Thread->>API: 3. Send Network Request over the internet
    API-->>Thread: 4. Return JSON Response
    Thread-->>EventLoop: 5. Hand the deserialized data back to the main program

Writing Production-Grade Code: The Resilient Fetcher

Let's put this into a realistic context. Imagine you're basically building a blazing-fast data aggregation dashboard for a startup, and you need to pull live statistics from an unreliable API, and

sometimes the server crashes and sends back an HTML error page instead of formatted JSON and it strictly blocks default Python scripts for prevent spam bots. If a beginner blindly calls .json() on the bad response, the code will throw nasty JSONDecodeError and an entire application will crash.

Here is how an expert handles this by combining threading safe deserialization, and headers into one robust block about code:

import asyncio
import requests

def fetch_data_sync(url, token):
    """A standard, blocking function that safely fetches JSON."""
    # We pass headers as metadata envelopes to bypass bot blockers and authorize our request
    headers = {
        "User-Agent": "AnalyticsDashboard/1.0",
        "Authorization": f"Bearer {token}"
    }

    try:
        # Prevent the program from waiting forever by enforcing a timeout
        response = requests.get(url, headers=headers, timeout=5)

        # Ensure the server actually responded with a success code
        response.raise_for_status()

        # Safe deserialization: turn the flat text into a living Python Dictionary
        return response.json()

    except requests.exceptions.RequestException:
        # Catch network errors and timeouts gracefully
        return None
    except ValueError:
        # Protect the code from crashing when the server returns flat HTML instead of JSON
        return None

async def main():
    api_urls = ["https://api.example.com/data1", "https://api.example.com/data2"]
    api_token = "super_secret_token"

    # We use asyncio.to_thread to run our blocking function in a separate thread
    # Then we use asyncio.gather() to run all tasks at the exact same time
    tasks = [asyncio.to_thread(fetch_data_sync, url, api_token) for url in api_urls]

    results = await asyncio.gather(*tasks)
    print("Dashboard Data:", results)

# Typically, application developers use asyncio.run() to execute the event loop
if __name__ == "__main__":
    asyncio.run(main())

Notice how beautifully this all comes together! We use headers for pass custom metadata envelopes, we safely unpack the universal language of flat JSON text, and we wrap it all in cutting-edge async threads.

What's Next?

Today, we solved the massive problem of freezing programs. We learned that threads are perfect towards I/O-bound tasks where we spend most of our time waiting, and we used asyncio.to_thread() towards effortlessly weave standard code into modern event loops, while

but remember our discussion about the Global Interpreter Lock (GIL)? We established that threads can't achieve true parallelism for heavy CPU-bound tasks; so, what if we want to build desktop application to parallel image processing, or run complex mathematical calculations that actually require the CPU to do multiple things at the exact same physical time?

To bypass the GIL completely, we need a different approach. In our next chapter we're basically going to dive into Python Multiprocessing. We will cover it next exploring how to unlock the true parallel power of your computer's CPU cores. See you there!

Learn Together
Session active! Discuss with other learners.
No notes yet. Select text in the concept body to add a note.