Think of threads as tiny workers inside your Python program. Each one has a job to do, like fetching water or delivering messages. Just like people, threads have a lifecycle—they start out new, get to work running their tasks, and then finish when their job is done. In this article, we’ll follow these little workers on their journey: how they come alive, run their tasks, and then rest when finished. It’s like watching a team of busy helpers doing their thing behind the scenes.
Creating a New Thread (The Birth)
Before a thread starts running, it has to be created—kind of like a worker getting hired but not clocked in yet. In Python, we create a thread using threading.Thread
, where we tell it what job to do by giving it a function. At this point, the thread is brand new. It hasn’t started working yet, so it’s not “alive.”
In the example below, we define a simple function called ready()
that just prints a message. Then we make a thread that will run that function. But we don’t start the thread yet—we just create it and check if it’s alive. Since we haven’t called .start()
, it’s not doing anything yet.
import threading
def ready():
print("I'm ready to start!")
t = threading.Thread(target=ready)
print(f"Thread is alive? {t.is_alive()}") # False - just born, not started
At this stage, the thread is in its “New” state—it’s all set up and waiting to be told, “Go!”
Starting and Running Threads (The Work Phase)
Once a thread is created, it’s time to put it to work. To do that, we call .start()
on the thread. This moves it from the “New” state to the “Running” state. Behind the scenes, Python calls the thread’s run()
method for us, which runs the function we gave it.
We can use .is_alive()
to check if the thread is currently doing its job. It returns True
if the thread is still running and False
if it has finished.
In this fun example, we have a thread that counts down from 3. Before we start it, it’s not alive. Once we call .start()
, it begins counting, and while it’s doing that, .is_alive()
is True
. After the countdown and the thread finishes its job, .is_alive()
goes back to False
.
import threading
import time
def countdown():
for i in range(3, 0, -1):
print(f"Counting down: {i}")
time.sleep(0.5)
t = threading.Thread(target=countdown)
print(t.is_alive()) # False
t.start
print(t.is_alive()) # True
t.join()
print(t.is_alive()) # False
This shows the thread’s journey as it wakes up, does its task, and then finishes. Like a worker showing up, doing a countdown, and then heading home.
Joining Threads (Waiting for Work to Finish)
Sometimes you want to wait for all your threads to finish before your program moves on—like making sure all the cookies are baked before you serve them. That’s where .join()
comes in. It tells Python, “Wait here until this thread is done.”
In the example below, we have two cookie types: Chocolate Chip and Oatmeal. Each one takes a different amount of time to bake, and we start both threads at the same time. Then, we loop through the list of threads and .join()
each one, making sure the main program waits for all the baking to finish before printing the final message.
import threading
import time
def bake(name, secs):
print(f"{name} started baking")
time.sleep(secs)
print(f"{name} finished baking")
threads = []
for name, secs in [("Chocolate Chip", 2), ("Oatmeal", 1)]:
t = threading.Thread(target=bake, args=(name, secs))
t.start()
threads.append(t)
for t in threads:
t.join()
print("All cookies are ready!")
So just like waiting in the kitchen until both trays are done, .join()
lets your main thread pause until all the helpers finish their baking tasks.
Thread States and Checking Status
Threads don’t come with visible labels like “New” or “Finished,” but you can still check what they’re up to using .is_alive()
. This handy method tells you whether a thread is still running its task or if it’s all done.
In the example below, we use .is_alive()
to peek at a thread’s status at three points: before it starts, right after it starts, and after it finishes. Inside the thread, we simulate a task by sleeping for a moment.
import threading
import time
def task():
print("Task started")
time.sleep(1)
print("Task finished")
t = threading.Thread(target=task)
print(f"Before start: alive? {t.is_alive()}") # Should be False
t.start()
print(f"Just after start: alive? {t.is_alive()}") # Likely True
t.join()
print(f"After join: alive? {t.is_alive()}") # Should be False
This shows how a thread moves through its lifecycle. It starts as not alive, becomes alive while working, then not alive again once it’s done. It’s like catching a worker before their shift, during the job, and after they’ve clocked out.
Daemon Threads and Their Lifecycle
Daemon threads are like background helpers that quietly do their job, but don’t stop the main program from ending. They follow the same basic lifecycle—start, run, finish—but with one big twist: if the main thread finishes, daemon threads are stopped automatically, even if they’re still working.
This makes daemon threads great for background tasks that shouldn’t hold up your program, like logging, listening, or—in this case—standing guard.
Here’s an example where we create a “night guard” thread that keeps watch in the background. It runs in an endless loop, but since it’s a daemon, Python will automatically end it when the main thread finishes:
import threading
import time
def night_guard():
while True:
print("Night guard watching...")
time.sleep(1)
t = threading.Thread(target=night_guard, daemon=True)
t.start()
time.sleep(3)
print("Main program done. Night guard fades away.")
Note: Depending on how your Python environment handles output and shutdown, it might look like the background thread keeps running. However, because it’s a daemon, Python will stop it once the main thread finishes. Normally, the program should exit cleanly right after the final message.
Thread Lifecycle with Thread Classes (OOP Style)
You can also manage threads using object-oriented programming by subclassing threading.Thread
. This gives you more structure, especially when you want to add more behavior or reuse thread logic. Just override the run()
method to define what the thread should do when it’s running.
In this example, we create a Greeter
thread class. When it runs, it simply prints a hello message. We then check its lifecycle using .is_alive()
before starting, during running, and after it finishes.
import threading
class Greeter(threading.Thread):
def run(self):
print("Hello! Thread is running.")
t = Greeter()
print(t.is_alive()) # False
t.start()
print(t.is_alive()) # True
t.join()
print(t.is_alive()) # False
This works just like regular threads—only with a bit more style! The Greeter
thread is born, starts its job, says hello, and finishes—all while you track its status.
Conclusion
And that’s the life of a thread—short, focused, and helpful! It starts in the New state when created, moves into Running when started, and ends in the Finished state after completing its job. Along the way, we saw how to create threads, check their status, wait for them with .join()
, use daemon threads for background tasks, and even write thread classes with a touch of OOP.
Threads are like tiny workers: they come alive, do their task, and quietly step away when done. As you get more comfortable, you can dive deeper into cool tools like locks, queues, and thread-safe patterns. But for now, you’ve got a solid start on understanding the thread lifecycle—one step (or thread) at a time.