This page documents thread-safety guarantees for built-in types in Python’s free-threaded build. The guarantees described here apply when using Python with the
disabled (free-threaded mode). When the GIL is enabled, most operations are implicitly serialized.
For general guidance on writing thread-safe code in free-threaded Python, see
Python support for free threading
.
Thread safety levels
The C API documentation uses the following levels to describe the thread safety guarantees of each function. The levels are listed from least to most safe.
Incompatible
A function or operation that cannot be made safe for concurrent use even with external synchronization. Incompatible code typically accesses global state in an unsynchronized way and must only be called from a single thread throughout the program’s lifetime.
Example: a function that modifies process-wide state such as signal handlers or environment variables, where concurrent calls from any threads, even with external locking, can conflict with the runtime or other libraries.
Compatible
A function or operation that is safe to call from multiple threads provided the caller supplies appropriate external synchronization, for example by holding a
for the duration of each call. Without such synchronization, concurrent calls may produce
or
.
Example: a function that reads from or writes to an object whose internal state is not protected by a lock. Callers must ensure that no two threads access the same object at the same time.
Safe on distinct objects
A function or operation that is safe to call from multiple threads without external synchronization, as long as each thread operates on a different object. Two threads may call the function at the same time, but they must not pass the same object (or objects that share underlying state) as arguments.
Example: a function that modifies fields of a struct using non-atomic writes. Two threads can each call the function on their own struct instance safely, but concurrent calls on the same instance require external synchronization.
Safe on shared objects
A function or operation that is safe for concurrent use on the same object. The implementation uses internal synchronization (such as
or
) to protect shared mutable state, so callers do not need to supply their own locking.
Example:
can be called from multiple threads on the same
- it uses internal synchronization to serialize access.
Atomic
A function or operation that appears
with respect to other threads - it executes instantaneously from the perspective of other threads. This is the strongest form of thread safety.
Example:
performs an atomic read of the mutex state and can be called from any thread at any time.
Thread safety for list objects
Reading a single element from a
is
:
lst[i]# list.__getitem__The following methods traverse the list and use
reads of each item to perform their function. That means that they may return results affected by concurrent modifications:
iteminlstlst.index(item)lst.count(item)All of the above operations avoid acquiring
. They do not block concurrent modifications. Other operations that hold a lock will not block these from observing intermediate states.
All other operations from here on block using the
.
Writing a single item via lst[i]=x is safe to call from multiple threads and will not corrupt the list.
The following operations return new objects and appear
to other threads:
lst1+lst2# concatenates two lists into a new listx*lst# repeats lst x times into a new listlst.copy()# returns a shallow copy of the listThe following methods that only operate on a single element with no shifting required are
:
lst.append(x)# append to the end of the list, no shifting requiredlst.pop()# pop element from the end of the list, no shifting requiredThe
method is also
. Other threads cannot observe elements being removed.
The
method is not
. Other threads cannot observe intermediate states during sorting, but the list appears empty for the duration of the sort.
The following operations may allow
operations to observe intermediate states since they modify multiple elements in place:
lst.insert(idx,item)# shifts elementslst.pop(idx)# idx not at the end of the list, shifts elementslst*=x# copies elements in placeThe
method may allow concurrent modifications since element comparison may execute arbitrary Python code (via
).
is safe to call from multiple threads. However, its guarantees depend on the iterable passed to it. If it is a
, a
, a
, a
, a
or a
(but not their subclasses), the extend operation is safe from concurrent modifications to the iterable. Otherwise, an iterator is created which can be concurrently modified by another thread. The same applies to inplace concatenation of a list with other iterables when using lst+=iterable.
Similarly, assigning to a list slice with lst[i:j]=iterable is safe to call from multiple threads, but iterable is only locked when it is also a
(but not its subclasses).
Operations that involve multiple accesses, as well as iteration, are never atomic. For example:
# NOT atomic: read-modify-writelst[i]=lst[i]+1# NOT atomic: check-then-actiflst:item=lst.pop()# NOT thread-safe: iteration while modifyingforiteminlst:process(item)# another thread may modify lstConsider external synchronization when sharing
instances across threads.
Thread safety for dict objects
Creating a dictionary with the
constructor is atomic when the argument to it is a dict or a
. When using the
method, dictionary creation is atomic when the argument is a dict, tuple,
or
.
The following operations and functions are
and
.
d[key]# dict.__getitem__d.get(key)# dict.getkeyind# dict.__contains__len(d)# dict.__len__All other operations from here on hold the
.
Writing or removing a single item is safe to call from multiple threads and will not corrupt the dictionary:
d[key]=value# writedeld[key]# deleted.pop(key)# remove and returnd.popitem()# remove and return last itemd.setdefault(key,v)# insert if missingThese operations may compare keys using
, which can execute arbitrary Python code. During such comparisons, the dictionary may be modified by another thread. For built-in types like
,
, and
, that implement __eq__() in C, the underlying lock is not released during comparisons and this is not a concern.
The following operations return new objects and hold the
for the duration of the operation:
d.copy()# returns a shallow copy of the dictionaryd|other# merges two dicts into a new dictd.keys()# returns a new dict_keys view objectd.values()# returns a new dict_values view objectd.items()# returns a new dict_items view objectThe
method holds the lock for its duration. Other threads cannot observe elements being removed.
The following operations lock both dictionaries. For
and |=, this applies only when the other operand is a
that uses the standard dict iterator (but not subclasses that override iteration). For equality comparison, this applies to dict and its subclasses:
d.update(other_dict)# both locked when other_dict is a dictd|=other_dict# both locked when other_dict is a dictd==other_dict# both locked for dict and subclassesAll comparison operations also compare values using
, so for non-built-in types the lock may be released during comparison.
locks both the new dictionary and the iterable when the iterable is exactly a
,
, or
(not subclasses):
dict.fromkeys(a_dict)# locks bothdict.fromkeys(a_set)# locks bothdict.fromkeys(a_frozenset)# locks bothWhen updating from a non-dict iterable, only the target dictionary is locked. The iterable may be concurrently modified by another thread:
d.update(iterable)# iterable is not a dict: only d lockedd|=iterable# iterable is not a dict: only d lockeddict.fromkeys(iterable)# iterable is not a dict/set/frozenset: only result lockedOperations that involve multiple accesses, as well as iteration, are never atomic:
# NOT atomic: read-modify-writed[key]=d[key]+1# NOT atomic: check-then-act (TOCTOU)ifkeyind:deld[key]# NOT thread-safe: iteration while modifyingforkey,valueind.items():process(key)# another thread may modify dTo avoid time-of-check to time-of-use (TOCTOU) issues, use atomic operations or handle exceptions:
# Use pop() with default instead of check-then-deleted.pop(key,None)# Or handle the exceptiontry:deld[key]exceptKeyError:passTo safely iterate over a dictionary that may be modified by another thread, iterate over a copy:
# Make a copy to iterate safelyforkey,valueind.copy().items():process(key)Consider external synchronization when sharing
instances across threads.
Thread safety for set objects
The
function is lock-free and
.
The following read operation is lock-free. It does not block concurrent modifications and may observe intermediate states from operations that hold the per-object lock:
elemins# set.__contains__This operation may compare elements using
, which can execute arbitrary Python code. During such comparisons, the set may be modified by another thread. For built-in types like
,
, and
, __eq__() does not release the underlying lock during comparisons and this is not a concern.
All other operations from here on hold the per-object lock.
Adding or removing a single element is safe to call from multiple threads and will not corrupt the set:
s.add(elem)# add elements.remove(elem)# remove element, raise if missings.discard(elem)# remove element if presents.pop()# remove and return arbitrary elementThese operations also compare elements, so the same
considerations as above apply.
The
method returns a new object and holds the per-object lock for the duration so that it is always atomic.
The
method holds the lock for its duration. Other threads cannot observe elements being removed.
The following operations only accept
or
as operands and always lock both objects:
s|=other# other must be set/frozensets&=other# other must be set/frozensets-=other# other must be set/frozensets^=other# other must be set/frozensets&other# other must be set/frozensets|other# other must be set/frozensets-other# other must be set/frozensets^other# other must be set/frozenset
,
,
and
can take multiple iterables as arguments. They all iterate through all the passed iterables and do the following:
and
lock both objects only whenthe other operand is a
,
, or
.
and
always try to lockall objects.
tries to lock both objects.
The update variants of the above methods also have some differences between them:
and
tryto lock all objects one-by-one.
set.symmetric_difference_update()
only locks the arguments if it isof type
,
, or
.
The following methods always try to lock both objects:
s.isdisjoint(other)# both lockeds.issubset(other)# both lockeds.issuperset(other)# both lockedOperations that involve multiple accesses, as well as iteration, are never atomic:
# NOT atomic: check-then-actifelemins:s.remove(elem)# NOT thread-safe: iteration while modifyingforelemins:process(elem)# another thread may modify sConsider external synchronization when sharing
instances across threads. See
Python support for free threading
for more information.
Thread safety for bytearray objects
The
function is lock-free and
.
Concatenation and comparisons use the buffer protocol, which prevents resizing but does not hold the per-object lock. These operations may observe intermediate states from concurrent modifications:
ba+other# may observe concurrent writesba==other# may observe concurrent writesba<other# may observe concurrent writesAll other operations from here on hold the per-object lock.
Reading a single element or slice is safe to call from multiple threads:
ba[i]# bytearray.__getitem__ba[i:j]# sliceThe following operations are safe to call from multiple threads and will not corrupt the bytearray:
ba[i]=x# write single byteba[i:j]=values# write sliceba.append(x)# append single byteba.extend(other)# extend with iterableba.insert(i,x)# insert single byteba.pop()# remove and return last byteba.pop(i)# remove and return byte at indexba.remove(x)# remove first occurrenceba.reverse()# reverse in placeba.clear()# remove all bytesSlice assignment locks both objects when values is a
:
ba[i:j]=other_bytearray# both lockedThe following operations return new objects and hold the per-object lock for the duration:
ba.copy()# returns a shallow copyba*n# repeat into new bytearrayThe membership test holds the lock for its duration:
xinba# bytearray.__contains__All other bytearray methods (such as
,
,
,
, etc.) hold the per-object lock for their duration.
Operations that involve multiple accesses, as well as iteration, are never atomic:
# NOT atomic: check-then-actifxinba:ba.remove(x)# NOT thread-safe: iteration while modifyingforbyteinba:process(byte)# another thread may modify baTo safely iterate over a bytearray that may be modified by another thread, iterate over a copy:
# Make a copy to iterate safelyforbyteinba.copy():process(byte)Consider external synchronization when sharing
instances across threads. See
Python support for free threading
for more information.
Thread safety for memoryview objects
objects provide access to the internal data of an underlying object without copying. Thread safety depends on both the memoryview itself and the underlying buffer exporter.
The memoryview implementation uses atomic operations to track its own exports in the
. Creating and releasing a memoryview are thread-safe. Attribute access (e.g.,
,
) reads fields that are immutable for the lifetime of the memoryview, so concurrent reads are safe as long as the memoryview has not been released.
However, the actual data accessed through the memoryview is owned by the underlying object. Concurrent access to this data is only safe if the underlying object supports it:
For immutable objects like
, concurrent reads through multiple memoryviews are safe.
For mutable objects like
, reading and writing the same memory region from multiple threads without external synchronization is not safe and may result in data corruption. Note that even read-only memoryviews of mutable objects do not prevent data races if the underlying object is modified from another thread.
# NOT safe: concurrent writes to the same bufferdata=bytearray(1000)view=memoryview(data)# Thread 1: view[0:500] = b'x' * 500# Thread 2: view[0:500] = b'y' * 500# Safe: use a lock for concurrent accessimportthreadinglock=threading.Lock()data=bytearray(1000)view=memoryview(data)withlock:view[0:500]=b'x'*500Resizing or reallocating the underlying object (such as calling
) while a memoryview is exported raises
. This is enforced regardless of threading.