Python 3.12 ยท
โ verified by execution on 2026-07-30
Threading in Python allows you to run multiple operations concurrently in a single process. However, the presence of the Global Interpreter Lock (GIL) fundamentally changes how Python threads behave compared to true parallel threads in other languages like C++ or Java.
In this lesson, we will explore how to manage concurrency, overcome I/O wait times, and write thread-safe logic using Locks.
Threading vs Multiprocessing
Multiprocessing gives each process its own interpreter and its own core. Threads share one process โ and one GIL, so only one thread runs Python bytecode at a time.
Threading Basics
You give a Thread its work in one of two ways: pass a callable as target=, or subclass Thread and override run(). The first is almost always the simpler choice.
Adjust the slider above to see how increasing worker threads drastically reduces overall completion time when tasks are waiting on I/O!
Shared Memory & Race Conditions
All threads live inside one process and share the same memory. That is what makes them cheap to start and easy to pass data between โ and simultaneously what makes them dangerous, because two threads can reach the same variable at once.
Threads share memory and can modify the same variables. Note: in Python 3.10+ due to GIL optimizations, a simple += 1 might not always trigger a visible race condition with small loop counts, but it is fundamentally unsafe without a lock.
python
import threadingcounter = 0def increment(): global counter for _ in range(10000): counter += 1threads = [threading.Thread(target=increment) for _ in range(2)]for t in threads: t.start()for t in threads: t.join()print(f'Counter is roughly {counter}')
Output
Counter is roughly 20000
Your output
Using Locks (Context Managers)
Use a Lock as a context manager โ with lock: โ rather than calling acquire() and release() by hand. The with form releases even when the block raises; a manual release() on the error path is the classic source of a hung program.
Using a Lock as a context manager to synchronize access to shared state.
python
import threadingcounter = 0lock = threading.Lock()def safe_increment(): global counter for _ in range(10000): with lock: counter += 1threads = [threading.Thread(target=safe_increment) for _ in range(2)]for t in threads: t.start()for t in threads: t.join()print(f'Safe counter is exactly {counter}')
Output
Safe counter is exactly 20000
Your output
Check yourself
What does the GIL prevent?
Reveal answer
Two threads executing Python bytecode simultaneously โ Only one thread runs Python bytecode at a time. Threads still exist and interleave โ they just do not run Python in parallel.
Which workload does threading genuinely speed up?
Reveal answer
I/O-bound work, where threads sit waiting on network or disk โ The GIL is released while waiting on I/O, so other threads run. For CPU-bound work you need multiprocessing.
Why is counter += 1 unsafe across threads?
Reveal answer
It is read-modify-write, so a thread can be interrupted between the read and the write โ The statement is three operations. Interleaving them loses updates, which is precisely what a Lock prevents.
What is the safest way to use a threading.Lock?
Reveal answer
Use it as a context manager: with lock: โ with lock: releases even when an exception is raised. A manual release() is skipped on the error path, which deadlocks the program.
Challenges
Challenge 1 +100 XP
Write a function `run_in_background(func, arg)` that takes a function and a single argument, spawns a new thread to run `func(arg)`, starts the thread, and returns the thread object without waiting for it to finish.
python
Test 1 โ expects "Hello\nTrue\n"
Need a hint? (โ25% XP)
Don't forget the comma in `args=(arg,)` to make it a tuple!
Show solution (0 XP)
import threadingdef run_in_background(func, arg): t = threading.Thread(target=func, args=(arg,)) t.start() return tdef print_msg(m): print(m)t = run_in_background(print_msg, 'Hello')t.join()print(type(t) is threading.Thread)
Challenge 2 +100 XP
Complete the `BankAccount` class by adding a threading lock so that `deposit` and `withdraw` are thread-safe. Initialize the lock in `__init__` and use it as a context manager.
python
Test 1 โ expects "True\n"
Need a hint? (โ25% XP)
Create `self.lock = threading.Lock()` and use `with self.lock:` around the state modifications.