python multithreading class-methods concurrency threading

Run Class methods in threads python

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In Python, a thread can run any callable, including an instance method, a class method, or a static method. The important detail is that you pass the bound method itself as the thread target and then manage shared state carefully, because the thread machinery is easy while the concurrency bugs are not.

Running an Instance Method in a Thread

An instance method is the most common case. If worker.run is the method you want, pass it directly as the target.

python
1import threading
2import time
3
4
5class Worker:
6    def __init__(self, name):
7        self.name = name
8
9    def run(self, seconds):
10        print(f"{self.name} starting")
11        time.sleep(seconds)
12        print(f"{self.name} finished")
13
14
15worker = Worker("job-1")
16thread = threading.Thread(target=worker.run, args=(2,))
17
18thread.start()
19thread.join()

Because worker.run is already bound to the instance, Python automatically supplies self. You only pass the remaining method arguments through args.

Running a @classmethod

Class methods work the same way, except the method is bound to the class instead of an instance.

python
1import threading
2import time
3
4
5class Reporter:
6    @classmethod
7    def emit(cls, label):
8        print(f"{cls.__name__} handling {label}")
9        time.sleep(1)
10
11
12thread = threading.Thread(target=Reporter.emit, args=("sync-task",))
13thread.start()
14thread.join()

Here, Python supplies cls automatically because Reporter.emit is a bound class method.

Running Several Methods Concurrently

If you want to fan out multiple workers, keep track of the thread objects and join them all:

python
1import threading
2import time
3
4
5class Downloader:
6    def fetch(self, url):
7        print(f"Fetching {url}")
8        time.sleep(1)
9        print(f"Done: {url}")
10
11
12downloader = Downloader()
13urls = ["https://example.com/a", "https://example.com/b", "https://example.com/c"]
14threads = []
15
16for url in urls:
17    t = threading.Thread(target=downloader.fetch, args=(url,))
18    t.start()
19    threads.append(t)
20
21for t in threads:
22    t.join()

This pattern is common for I/O-bound tasks such as network requests, file reads, or waiting on external services.

Shared State Needs Protection

Starting threads is easy. Sharing mutable data safely is harder. If multiple threads update the same attribute, use a lock.

python
1import threading
2
3
4class Counter:
5    def __init__(self):
6        self.value = 0
7        self.lock = threading.Lock()
8
9    def increment(self, times):
10        for _ in range(times):
11            with self.lock:
12                self.value += 1
13
14
15counter = Counter()
16threads = [
17    threading.Thread(target=counter.increment, args=(100_000,)),
18    threading.Thread(target=counter.increment, args=(100_000,)),
19]
20
21for t in threads:
22    t.start()
23
24for t in threads:
25    t.join()
26
27print(counter.value)

Without the lock, the final count can be lower than expected because increments are not automatically atomic at the application level.

Threads Are Best for I/O-Bound Work

Python threads are most useful when the task spends time waiting: HTTP calls, sockets, subprocesses, or disk I/O. For CPU-bound work, threads often do not speed things up much because of the GIL in standard CPython.

If the workload is CPU-heavy, consider:

  • 'multiprocessing for parallel CPU execution'
  • 'concurrent.futures.ThreadPoolExecutor for cleaner thread orchestration'
  • 'asyncio when the workload is asynchronous I/O rather than threaded blocking calls'

The method can still belong to a class in all of these cases; the scheduling model is what changes.

Common Pitfalls

The most common mistake is calling the method instead of passing it. This is wrong:

python
threading.Thread(target=worker.run())

That executes run() immediately on the main thread and passes its return value as the target. The correct form is target=worker.run.

Another issue is forgetting that instance methods share the instance. If several threads mutate self at the same time, race conditions are possible unless you synchronize access.

Finally, do not expect threads to accelerate CPU-bound Python loops automatically. They are excellent for I/O-bound concurrency, but a different execution model is usually better for true parallel computation.

Summary

  • A thread target can be an instance method, class method, or static method.
  • Pass the method itself as target, not the result of calling it.
  • Use args for the method parameters that remain after self or cls.
  • Protect shared mutable state with locks or another synchronization strategy.
  • Prefer threads for I/O-bound work, not for heavy CPU-bound loops in CPython.

Course illustration
Course illustration

All Rights Reserved.