Daniel Vahla: Preparing for a Free-Threaded Python World
Talk by Daniel Vahla at Plone Conference 2025 and PyCon Finland in Jyväskylä, Finland.
Slides are here: https://pycon2025.vahla.fi
This is a recap on thread safety and synchronisation primitives. Python is going to a free-threaded model.
I am consultant at Mavericks Software, 7 years of Python experience.
Why does this talk matter? Python 3.14 has an opt-in with GIL-free builds, via python3.14t
. True parallism means real race conditions. We need to protect ourselves in ways that the GIL (global interpreter lock) did for us.
Without synchronization, multiple threads can touch the same data.
Demo: game of throwing dice by three players, each in a separate thread. A player plays until they have 20 points. This goes wrong, only one of the three throws gets used for all three.
So we add a Lock. Only one thread can hold the lock at a time. Others wait until it is released. This works.
We can use RLock: Recursive locking. With a regular lock you cannot acquire the same lock twice in same thread. In our game we will say: if you roll 6, you can roll again. This needs the RLock.
Demo 3: semaphore. This is like a lock with a counter, allowing limited access. N threads can have access at the same time. For example used for connection pools, resource limits, rate limiting. In our game we will allow at most 2 players to play the game. On join, we acquire the semaphore, and when finished, we release it.
Demo 4: BoundedSemaphore. You may need a defensive semaphore. This is for bug detection. Semaphore can release more than acquired. For best practice: always use the BoundedSemaphore.
Demo 5: Event. A simple signal. A boolean flag. Threads can wait()
for event. One thread callse set()
to wake all. We use this in our game to wait until all threads have been created.
Demo 6: Condition. For complex coordination: a Wait plus Notify. wait()
releases locks and blocks. notify()
or notify_all()
wakes the waiters. You should always use this in a while loop, to avoid spurious wakeups. Used for producer-consumer patterns. We will use this to wait for enough players to join.
Demo 7: Barrier. For Phase Sync. Cleaner than Condition for rounds. Created with a party count: Barrier(n)
. You can have optional action callbacks. Used for multi-phase algorithms, round-based games, synchronizing computational stages. We use it in our game to wait for 3 players and play 3 rounds. In the callback we will print that a new round has started.
Key takeaways:
- GIL-free is here, opt-in
- Thread safety is critical
- Know your primitives, use the right tool for the job
- Test now with
python3.14t
. - Always use
BoundedSemaphore
overSemaphore
. - If you are not sure if something is safe, just use a Lock.