threading
Python
thread-locking
concurrency
synchronization

Python threading. How do I lock a thread?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

In Python, you usually do not "lock a thread" directly. What you lock is a shared resource or a critical section so that only one thread can use it at a time.

What a Lock Really Does

A threading.Lock is a synchronization primitive. One thread acquires it, enters the critical section, and later releases it. If another thread reaches the same lock while it is held, that thread waits.

This is how you protect shared state from race conditions.

python
1import threading
2import time
3
4counter = 0
5lock = threading.Lock()
6
7
8def increment():
9    global counter
10
11    for _ in range(100_000):
12        with lock:
13            counter += 1
14
15
16threads = [threading.Thread(target=increment) for _ in range(2)]
17
18for thread in threads:
19    thread.start()
20
21for thread in threads:
22    thread.join()
23
24print(counter)

Without the lock, counter += 1 can interleave unpredictably between threads.

The Right Mental Model

If you say "I want to lock a thread," the real question is usually one of these:

  • how do I prevent two threads from editing the same data at once
  • how do I pause one thread until another thread finishes something
  • how do I ensure only one thread enters a block of code

Those are solved with synchronization primitives, not by freezing a thread object itself.

Basic Lock Usage

The cleanest style is the context-manager form:

python
1import threading
2
3lock = threading.Lock()
4
5def write_shared_list(shared_list, value):
6    with lock:
7        shared_list.append(value)

Using with lock: is better than manually calling acquire() and release() because it guarantees the release even if an exception happens inside the block.

If you need manual control, you can still write:

python
1lock.acquire()
2try:
3    # critical section
4    pass
5finally:
6    lock.release()

Reentrant Locks and Other Primitives

Sometimes a plain Lock is not enough.

Use threading.RLock when the same thread may need to acquire the same lock more than once:

python
1import threading
2
3lock = threading.RLock()
4
5def outer():
6    with lock:
7        inner()
8
9def inner():
10    with lock:
11        print("safe re-entry")

Use a Semaphore when you want to allow a limited number of threads through at once instead of exactly one.

Use an Event when one thread should wait until another thread signals that work is ready.

python
1import threading
2import time
3
4ready = threading.Event()
5
6def worker():
7    print("waiting")
8    ready.wait()
9    print("running")
10
11thread = threading.Thread(target=worker)
12thread.start()
13
14time.sleep(1)
15ready.set()
16thread.join()

That is not mutual exclusion, but it is often what people actually mean when they say they want to "lock" a thread.

What About the GIL

Python has a Global Interpreter Lock in CPython, but it does not remove the need for your own locks. The GIL does not make compound operations on your application data magically safe, and it does not coordinate the meaning of your critical sections.

You still need explicit synchronization for shared mutable state.

A Thread-Safe Queue Is Often Better

If threads are passing work to each other, a queue.Queue is often a better design than manually locking your own list.

python
1import queue
2import threading
3
4tasks = queue.Queue()
5
6def producer():
7    for i in range(5):
8        tasks.put(i)
9
10def consumer():
11    while True:
12        item = tasks.get()
13        print("processing", item)
14        tasks.task_done()
15        if item == 4:
16            break

The queue already handles the internal locking for you.

Common Pitfalls

The biggest pitfall is locking too much code. Keep the critical section as small as possible so threads do not block each other unnecessarily.

Another pitfall is forgetting to release a lock on error. That is why with lock: is the preferred pattern.

A third pitfall is trying to use a lock when the real need is coordination rather than exclusion. In those cases, Event, Condition, or Queue is usually the better tool.

Finally, avoid multiple locks with inconsistent acquisition order unless you are prepared to reason carefully about deadlocks.

Summary

  • In Python, you lock shared code or data, not the thread object itself
  • 'threading.Lock protects critical sections from concurrent access'
  • 'with lock: is the safest and clearest usage pattern'
  • Use RLock, Semaphore, Event, or Queue when the problem is not simple mutual exclusion
  • The GIL does not replace proper synchronization in your own program

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.