Last Updated on September 12, 2022
You can make thread-safe calls to print() using a mutex lock such as threading.Lock.
In this tutorial you will discover how to make calls to print() thread-safe in Python.
Let’s get started.
Need Thread-Safe Print
A thread is a thread of execution in a computer program.
Every Python program has at least one thread of execution called the main thread. Both processes and threads are created and managed by the underlying operating system.
Sometimes we may need to create additional threads in our program in order to execute code concurrently.
Python provides the ability to create and manage new threads via the threading module and the threading.Thread class.
You can learn more about Python threads in the guide:
In concurrent programming, we may want to print statements to the command line (also called the terminal or command prompt) via the print() built-in function.
This may be for many reasons, such as:
- Primary output from the program for the user.
- Debugging print statements for threads.
- Reporting progress of tasks in threads.
Is calling print() from multiple threads thread-safe? If not, how can we make print thread-safe?
Run loops using all CPUs, download your FREE book to learn how.
How to Thread-Safe Print
The print() function is a built-in function for printing a string on stdout and is not thread-safe.
The print() function takes a string message, or an object that can be converted to a string.
For example:
1 2 3 |
... # print a message to the command line print('Hi there') |
By default, messages are appended with a newline character ‘\n‘, although an alternate ending can be specified via the “end” argument.
An “end” value of None will not include a separator between messages, printing on the same line.
For example:
1 2 3 |
... # print a message without a new line print('Hello', end=None) |
By default, messages are not flushed to stdout until an internal buffer is full. Messages can be forced to flush after each call, which can be achieved by setting the “flush” argument to True.
For example:
1 2 3 |
... # always flush the stream print('Hello', flush=True) |
By default, it reports messages to stdout, although can be configured to output messages to any file via the “file” argument.
Recall that stdout refers to “standard out” or “standard output” which is the stream for output messages in the command line (also called terminal or command prompt).
Calls to print from multiple threads is not thread-safe and will likely result in the corruption of messages as threads are context switched.
In order to make the print() function thread-safe, it needs to be treated as a critical section and protected with a mutual exclusion (mutex) lock.
This can be achieved with an instance of the threading.Lock class.
First, an instance of the lock can be created.
1 2 3 |
... # create a lock lock = threading.Lock() |
The lock can then be shared with all threads that need to print.
Prior to printing, the thread must acquire the lock, call print, then release the lock.
For example:
1 2 3 4 5 6 7 |
... # acquire the lock lock.acquire() # report message print('...') # release the lock lock.release() |
Only one thread can hold the lock at a time. As only one thread can hold the lock, it means that only one thread can print at a time.
Other threads that attempt to acquire the lock while it is already acquired will automatically wait until the lock is available.
It is critical that the lock is released after it is no longer needed, to allow other threads to acquire it. Otherwise those threads waiting for the lock will never progress, resulting in a concurrency failure condition called a deadlock.
An easy way to acquire the lock and release it automatically is via the context manager interface for the threading.Lock class.
For example:
1 2 3 4 5 |
... # acquire the lock with lock: # report the message print('...') |
You can learn more about mutex locks in this tutorial:
Next, let’s look at some worked examples of printing in a thread-safe manner.
Example of Thread-Unsafe Print
Before we look at examples of thread-safe calls to print(), let’s look at an example of how calling print from multiple threads is not thread-safe.
In this example we will create 1,000 threads, each of which will generate a random number between 0 and 1, block for that fraction of a second, then report the value by calling print(). The print statements will step on each other and will result in a corrupted stream of messages.
First, we will define a function that takes a unique integer for the task, generates a random number by calling random.random(), blocks by calling time.sleep() then reports the number by calling print() with a formatted string.
The task() function below implements this.
1 2 3 4 5 6 7 8 |
# task for worker threads def task(number): # generate random number between 0 and 1 value = random() # block sleep(value) # report print(f'Thread {number} got {value}.') |
Next, in the main thread, we can create 1,000 instances of the threading.Thread class, each configured to call our task() function with a unique integer from 0 to 999.
We can do this in a list comprehension in order to create a list of threading.Thread objects.
1 2 3 |
... # configure many threads threads = [Thread(target=task, args=(i,)) for i in range(1000)] |
We can then call the start() method on each thread to start their execution, and then wait for all threads to finish by calling the join() method.
1 2 3 4 5 6 7 |
... # start threads for thread in threads: thread.start() # wait for threads to finish for thread in threads: thread.join() |
We expect a race condition in the print statement as all 1,000 threads will attempt to print messages concurrently.
The result will be a corruption of the messages that are printed, rather than a clean one message per line.
Note, this is still a problem, even if we set the “flush” argument to True, requiring that the stream always flush messages.
1 2 3 |
... # report print(f'Thread {number} got {value}.', flush=True) |
Tying this together, the complete example of print being not thread-safe is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
# SuperFastPython.com # example of print is not thread-safe from random import random from time import sleep from threading import Thread # task for worker threads def task(number): # generate random number between 0 and 1 value = random() # block sleep(value) # report print(f'Thread {number} got {value}.') # configure many threads threads = [Thread(target=task, args=(i,)) for i in range(1000)] # start threads for thread in threads: thread.start() # wait for threads to finish for thread in threads: thread.join() |
Running the example first creates the 1,000 threads and configures them to execute our task() function.
The threads are started and the main thread blocks until all threads are terminated.
Each thread generates a random number, blocks and reports its value.
The result is 1,000 messages printed concurrently to stdout.
A truncated example of the output is listed below.
Note, your specific results will differ each time the program is run due to the use of random numbers.
1 2 3 4 5 6 7 8 9 10 11 12 |
Thread 142 got 0.00021174829540115958. Thread 379 got 0.002046866376592793.Thread 231 got 0.009602303512181054. Thread 103 got 0.017722001062820292. Thread 175 got 0.01806447484727458. Thread 99 got 0.024806470811583714. Thread 146 got 0.02576741412800998. Thread 1 got 0.034946324325054845. Thread 357 got 0.02146115453610209. Thread 221 got 0.03013721679572312. Thread 385 got 0.021983019539983162.Thread 454 got 0.017871263686212946. ... |
This demonstrates that print() function is not thread-safe.
Next, let’s look at how we can call print() in a thread-safe manner.
Free Python Threading Course
Download your FREE threading PDF cheat sheet and get BONUS access to my free 7-day crash course on the threading API.
Discover how to use the Python threading module including how to create and start new threads and how to use a mutex locks and semaphores
Example of Thread-Safe Print
We can update the example in the previous section to be thread-safe.
This can be achieved by creating a mutex lock and sharing among all threads that need to print. The task() function can then be updated so that the lock must be acquired prior to calling the print statement.
First, we can update the task() function to take a lock as an argument.
1 2 3 |
# task for worker threads def task(number, lock): # ... |
Next, we can update the call to print() to be protected by the lock using the context manager interface.
1 2 3 4 |
... # report with lock: print(f'Thread {number} got {value}.') |
In the main thread, we can create an instance of the lock to be shared among all threads.
1 2 3 |
... # create a shared lock lock = Lock() |
We can then pass the lock as an argument to each thread when it is configured.
1 2 3 |
... # configure many threads threads = [Thread(target=task, args=(i,lock)) for i in range(1000)] |
Tying this together, the complete example of calling the print() function in a thread-safe manner is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# SuperFastPython.com # example of thread-safe print from random import random from time import sleep from threading import Thread from threading import Lock # task for worker threads def task(number, lock): # generate random number between 0 and 1 value = random() # block sleep(value) # report with lock: print(f'Thread {number} got {value}.') # create a shared lock lock = Lock() # configure many threads threads = [Thread(target=task, args=(i,lock)) for i in range(1000)] # start threads for thread in threads: thread.start() # wait for threads to finish for thread in threads: thread.join() |
First, a shared lock is created.
Running the example creates and configures 1,000 threads, passing the lock to each.
The main thread starts all the worker threads then waits for all threads to terminate.
Each thread generates its random number, blocks, then attempts to acquire the lock. Once acquired, the thread will report its message and then release the lock.
A truncated example of the output is listed below.
The result is a clean one message per line with no corruption of messages seen with the thread-unsafe version.
Note, your specific results will differ each time the program is run due to the use of random numbers.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Thread 42 got 0.005379425682188854. Thread 49 got 0.01657114573560836. Thread 430 got 0.0015830746201580537. Thread 179 got 0.014964866854160097. Thread 13 got 0.03264742234329243. Thread 298 got 0.019898887953053612. Thread 625 got 0.0033862268764470738. Thread 121 got 0.0318745316694633. Thread 480 got 0.012546040877055908. Thread 208 got 0.0286350084811009. Thread 346 got 0.023910737473838983. Thread 142 got 0.037197126768493516. ... |
Next, let’s look at an example of how we can print in a thread-safe manner using a dedicated thread.
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
Example of Dedicated Print Thread With Queue
Another approach to thread-safe printing is to use a single dedicated thread to perform all printing.
Because only one thread can call print(), it is guaranteed to be thread-safe.
This can be achieved by creating a queue shared among all threads that need to print. Threads can put messages on the queue. A dedicated thread for printing can be created that runs in the background and gets messages from the queue and calls print.
We can update the thread-unsafe version developed above to use this dedicated printing thread approach.
First, we can define a task function to be executed by the printing thread. This function will take the shared queue as an argument. It will loop forever and get messages from the queue by calling get() and report the messages by calling print().
The printer() function below implements this.
1 2 3 4 5 6 7 |
# task for printing thread def printer(queue): while True: # get messages message = queue.get() # print the message print(message) |
The printer thread will block until messages appear on the queue via the call to Queue.get(), meaning that it will be computationally efficient.
You can learn more about blocking calls in this tutorial:
Next, we can update our task() function executed by worker threads to take the shared queue as an argument and to put messages on the queue instead of calling print.
The updated task() function with these changes is listed below.
1 2 3 4 5 6 7 8 |
# task for worker threads def task(number, queue): # generate random number between 0 and 1 value = random() # block sleep(value) # report queue.put(f'Thread {number} got {value}.') |
Finally, in the main thread we can create an instance of a queue, such as the queue.Queue class. This is a thread-safe data structure on which messages as objects can be appended via put() and removed and retrieved via get().
1 2 3 |
... # create a shared queue queue = Queue() |
We can then create a new thread for printing. We can configure it to call our printer() function, to run in the background by setting “daemon” to True and to give it a useful name like ‘Printer‘.
1 2 3 4 |
... # create printing thread printer_thread = Thread(target=printer, args=(queue,), daemon=True, name='Printer') printer_thread.start() |
Because the printer thread is a daemon thread, it will run in the background and will not prevent the Python process from terminating once all worker threads and the main thread terminate.
You can learn more about daemon threads in this tutorial:
Finally, we can pass the instance of the shared queue to each worker thread as an argument.
1 2 3 |
... # configure many threads threads = [Thread(target=task, args=(i,queue)) for i in range(1000)] |
Tying this together, the complete example of using a dedicated printing thread is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# SuperFastPython.com # example of thread-safe print thread and queue from random import random from time import sleep from threading import Thread from queue import Queue # task for printing thread def printer(queue): while True: # get messages message = queue.get() # print the message print(message) # task for worker threads def task(number, queue): # generate random number between 0 and 1 value = random() # block sleep(value) # report queue.put(f'Thread {number} got {value}.') # create a shared queue queue = Queue() # create printing thread printer_thread = Thread(target=printer, args=(queue,), daemon=True, name='Printer') printer_thread.start() # configure many threads threads = [Thread(target=task, args=(i,queue)) for i in range(1000)] # start threads for thread in threads: thread.start() # wait for threads to finish for thread in threads: thread.join() |
Running the example first creates the shared queue.
It then creates and configures the printer thread and starts executing it in the background.
Next, the worker threads are created and configured. Finally, the main thread starts the worker threads and waits for them to terminate.
Each worker thread performs its task, generating a random number and blocking. It then puts its messages on the queue and terminates.
The printer thread runs in a loop, each iteration taking one message of the queue and then printing it. Because there is only a single printer thread, it is the only thread calling print(), which makes it thread-safe.
A truncated example of the output is listed below.
Note, your specific results will differ each time the program is run due to the use of random numbers.
1 2 3 4 5 6 7 8 9 10 11 |
Thread 41 got 0.0009937726430424565. Thread 295 got 0.013061216765592465. Thread 490 got 0.002759410610238766. Thread 98 got 0.022733778225223267. Thread 510 got 0.00517634891577845. Thread 593 got 5.049634999099162e-05. Thread 297 got 0.01532270411186587. Thread 512 got 0.0052914236882953825. Thread 380 got 0.014671486167968983. Thread 517 got 0.008783940658370293. ... |
Further Reading
This section provides additional resources that you may find helpful.
Python Threading Books
- Python Threading Jump-Start, Jason Brownlee (my book!)
- Threading API Interview Questions
- Threading Module API Cheat Sheet
I also recommend specific chapters in the following books:
- Python Cookbook, David Beazley and Brian Jones, 2013.
- See: Chapter 12: Concurrency
- Effective Python, Brett Slatkin, 2019.
- See: Chapter 7: Concurrency and Parallelism
- Python in a Nutshell, Alex Martelli, et al., 2017.
- See: Chapter: 14: Threads and Processes
Guides
- Python Threading: The Complete Guide
- Python ThreadPoolExecutor: The Complete Guide
- Python ThreadPool: The Complete Guide
APIs
References
Takeaways
You now know how to call print() in a thread-safe manner.
Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.
Do you have any questions?