Python Thread target: Passing the Right Callable
python thread target: Understand how threading.Thread target works, pass args and kwargs correctly, avoid the common callable mistake, and handle thread exceptions.
When you create a threading.Thread, the target parameter is the callable that runs when the thread starts. The most common mistake with python thread target is passing the result of a call instead of the callable itself, which executes the function in the main thread before the thread object even exists.
What the target Parameter Actually Expects
threading.Thread(target=...) expects a callable — a function, bound method, lambda, or any object implementing __call__. It does not expect the return value of a call. The thread invokes target(*args, **kwargs) inside its own execution context when start() is called.
import threading def work(): print("working") # Wrong: work() runs immediately in the main thread t = threading.Thread(target=work()) t.start() # Correct: the function object is passed t = threading.Thread(target=work) t.start()
The first version calls work() during Thread.__init__, prints working in the main thread, and passes None as the target. The resulting thread has nothing to execute. The second version passes the function object, and the new thread runs it when start() is called.
If target is None, run() returns immediately and the thread exits without doing anything. That is valid but usually indicates a bug unless you are subclassing Thread and overriding run().
Passing Arguments with args and kwargs
Most target functions need input. The args parameter is a tuple of positional arguments, and kwargs is a dictionary of keyword arguments. Both are optional and default to empty.
import threading def download(url, retries=3, timeout=30): print(f"downloading {url} with {retries} retries") t = threading.Thread( target=download, args=("https://example.com/data",), kwargs={"retries": 5}, ) t.start()
args must be a tuple, which is why the single-element form needs the trailing comma: ("https://example.com/data",). Passing a bare string such as args="https://example.com/data" does not raise an error; Python iterates over the string and passes each character as a separate positional argument. The function then receives "h", "t", "t", ... as separate arguments, which almost always fails or produces nonsense. The same applies to passing a list to args — each element becomes a separate argument.
Passing Bound Methods, Lambdas, and partials
The target does not have to be a module-level function. Bound methods work directly because they are callables that carry their instance.
import threading from functools import partial class Worker: def __init__(self, name): self.name = name def run(self, delay): print(f"{self.name} starting with delay {delay}") w = Worker("alpha") t1 = threading.Thread(target=w.run, args=(1,)) t1.start() t2 = threading.Thread(target=partial(w.run, 2)) t2.start() t3 = threading.Thread(target=lambda: w.run(3)) t3.start()
All three start a thread that calls w.run. partial and lambda both let you fix arguments ahead of time. partial is usually clearer because it keeps the callable and its arguments visible; a lambda that only wraps another call adds indirection without benefit. Use a lambda when you need to transform arguments or call several functions in sequence.
What Happens When the Target Raises an Exception
An exception raised inside the target terminates that thread, but it is not propagated to the main thread. join() does not re-raise it, and the main thread continues as if nothing happened.
import threading def broken(): raise RuntimeError("boom") t = threading.Thread(target=broken) t.start() t.join() print("main continues") # the exception is not visible here
By default, the traceback is printed to stderr through the interpreter's excepthook, and the thread exits. If you need to observe the failure, store it yourself. A simple approach is to wrap the target and record the exception in a shared structure:
import threading def run_with_result(target, results): try: results.append(("ok", target())) except Exception as exc: results.append(("error", exc)) results = [] def work(): raise ValueError("bad input") t = threading.Thread(target=run_with_result, args=(work, results)) t.start() t.join() status, value = results[0] print(status, value)
This pattern keeps the exception inside the thread's lifetime and lets the main thread inspect it after join(). Python 3.8 also added threading.excepthook, which lets you install a global handler for uncaught thread exceptions, but it does not give the main thread access to the exception object.
How start, run, and Daemon Threads Interact
start() creates the OS thread and, inside it, calls run(). The default run() implementation invokes self._target(*self._args, **self._kwargs). If you subclass Thread and override run(), the target parameter is ignored unless your override calls super().run().
Daemon threads do not prevent the interpreter from exiting. When the main thread finishes and only daemon threads remain, the process shuts down and non-daemon threads are blocked from completing. Set daemon=True only when the thread's work can be abandoned safely at shutdown:
import threading import time def poll(): while True: time.sleep(1) t = threading.Thread(target=poll, daemon=True) t.start()
Non-daemon threads, by contrast, keep the process alive until they finish, which can hang shutdown if the target blocks forever. The target function should be written so it can exit when the program needs to stop, for example by checking a shutdown flag or reading from a queue with a timeout.
Choosing Between target and Subclassing Thread
target is the right choice when the thread runs a single existing callable. It keeps the work separate from the thread machinery and makes the code easy to test without creating threads. Subclassing Thread and overriding run() makes sense when the thread itself is a domain object that carries state across multiple operations, but it couples the work to the threading API and makes unit testing harder because you cannot call the logic without constructing a thread.
class Poller(threading.Thread): def __init__(self, interval): super().__init__() self.interval = interval self.running = True def run(self): while self.running: time.sleep(self.interval)
This subclass is reasonable when Poller is a meaningful object in the application. For a one-off background task, threading.Thread(target=some_function) is simpler and keeps the function testable in isolation.
Thread Safety and the GIL
The target function runs concurrently with the main thread and with any other threads, so shared mutable state requires synchronization. A list append like the results example above is safe under the CPython GIL for a single append, but compound operations such as count += 1 are not atomic and need a Lock. The GIL also means that CPU-bound pure-Python code does not speed up when split across threads; the interpreter serializes bytecode execution. target is most useful for I/O-bound work — network requests, file reads, waiting on sockets — where the thread blocks and the GIL is released during the wait. For CPU-bound parallelism, use multiprocessing or a ProcessPoolExecutor instead of threads.