Multithreading in Python: A Beginner-Friendly Guide

Deepesh AgrawalDeepesh Agrawal
5 min read

Multithreading is a programming technique that allows a program to run multiple tasks at the same time, within a single process. Think of it as multitasking for your code—like a chef cooking several dishes at once, instead of finishing one before starting the next

Why Use Multithreading in Python?

  • It makes programs faster and more responsive, especially when dealing with tasks that spend a lot of time waiting, such as downloading files, reading/writing to disk, or fetching data from the internet.

  • Multithreading is ideal for I/O-bound tasks—jobs that wait for input/output operations (like network or file access), not for heavy calculations

How Does Multithreading Work in Python?

Python provides a built-in threading module to make working with threads easy.

  • Each thread runs a function independently, but all threads share the same memory space, making it easy to share data between them.

  • Threads are lighter than processes and start faster, but because they share memory, you need to be careful to avoid conflicts when multiple threads access the same data.

The Global Interpreter Lock (GIL): What You Need to Know

  • Python has a feature called the Global Interpreter Lock (GIL), which means only one thread can execute Python code at a time.

  • This sounds limiting, but for I/O-bound tasks, multithreading still provides big speedups because threads can run while others are waiting for I/O.

When Should You Use Multithreading?

Multithreading is best for:

  • Downloading many files or making lots of web requests at once (e.g., web scraping).

  • Reading/writing several files at the same time

  • Handling multiple users or network connections in a server

It’s not ideal for tasks that need a lot of CPU power (like crunching numbers or image processing). For those, use multiprocessing instead

How to Use Multithreading in Python

Here’s a super simple example:

import threading
import time

def print_numbers():
    for i in range(5):
        print(f"Number: {i}")
        time.sleep(1)

# Create a thread that runs the print_numbers function
thread = threading.Thread(target=print_numbers)
thread.start()

# Main program continues running while thread works
print("Main program continues...")

thread.join()  # Wait for thread to finish before exiting

What’s Happening Here?

  • We define a function to print numbers.

  • We create a thread to run that function.

  • The main program keeps going while the thread does its work.

  • join() makes the main program wait for the thread to finish before exiting

    Key Concepts and Tips

    • Thread class: Core of the threading module; each thread is an instance of this class.

    • start(): Begins the thread’s activity.

    • join(): Waits for the thread to finish.

    • Locks: Use these to prevent data corruption when multiple threads access shared data

Real-World Examples

  • Web Scraping: Fetching data from multiple websites at once3.

  • File Processing: Reading/writing multiple files in parallel.

  • Network Servers: Handling multiple users or connections at the same time.

Multithreading vs Multiprocessing

FeatureMultithreadingMultiprocessing
Best forI/O-bound tasksCPU-bound tasks
Memory usageLow (threads share memory)Higher (each process has its own)
Python GILAffectedNot affected
CommunicationEasy (shared memory)Harder (separate memory)

Final Thoughts

  • Multithreading is a powerful tool for making Python programs faster and more efficient—when used for the right kind of tasks.

  • Remember: Use it for I/O-bound jobs, not heavy computations.

  • The threading module makes it easy to get started, even for beginners.

Deeper Understanding of Multithreading in Python

How Threads Share Memory

All threads in a process share:

  • Global variables

  • File descriptors

  • Standard input/output

This shared state is useful for communication but risky:

  • If two threads write to the same variable at once, race conditions can occur.

  • For example:

      counter = 0
    
      def increment():
          global counter
          for _ in range(1000000):
              counter += 1  # Not thread-safe!
    
      t1 = threading.Thread(target=increment)
      t2 = threading.Thread(target=increment)
      t1.start()
      t2.start()
      t1.join()
      t2.join()
      print(counter)  # May not be 2,000,000!
    

    Avoiding Race Conditions: Locks

    Use a Lock from the threading module:

lock = threading.Lock()

def thread_safe_increment():
    global counter
    for _ in range(1000000):
        with lock:  # Automatically acquires/releases the lock
            counter += 1

More About the Global Interpreter Lock (GIL)

  • The GIL is a mutex that protects access to Python objects, preventing multiple native threads from executing Python bytecodes at once.

  • This limits the effectiveness of threads for CPU-bound tasks.

  • BUT: While one thread is waiting (e.g., on network or disk), the GIL is released, allowing another thread to run.

So, multithreading is still beneficial for:

  • Web scraping

  • Logging

  • File I/O

  • User input handling

But not for:

  • Image processing

  • Data crunching

  • ML model training

ThreadPoolExecutor (High-Level Alternative)

Python's concurrent.futures.ThreadPoolExecutor makes multithreading even easier:

from concurrent.futures import ThreadPoolExecutor

def fetch_url(url):
    # Simulate download
    print(f"Fetching {url}")
    time.sleep(2)
    return f"{url} data"

urls = ["http://example.com", "http://test.com"]

with ThreadPoolExecutor(max_workers=2) as executor:
    results = executor.map(fetch_url, urls)
    for result in results:
        print(result)

Benefits:

  • Cleaner syntax

  • Handles thread creation and management for you

  • Supports timeouts, exceptions, and return values

Best Practices

  • Use multithreading only if your bottlenecks are I/O-based.

  • Prefer ThreadPoolExecutor over raw threads for ease of use.

  • Use Queue for thread-safe communication between threads.

  • Avoid blocking calls in the main thread (especially in GUI apps).

Want More?

If you're ready to take it further:

  • Try asyncio for asynchronous I/O (great alternative to threading for I/O-bound tasks).

  • Learn about queue.Queue for producer-consumer models.

  • Explore Python’s multiprocessing module for parallel CPU-bound work.

10
Subscribe to my newsletter

Read articles from Deepesh Agrawal directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Deepesh Agrawal
Deepesh Agrawal

building shits