Thread Safety Guarantees¶
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 GIL 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 for list objects¶
Reading a single element from a list is
atomic:
lst[i] # list.__getitem__
The following methods traverse the list and use atomic reads of each item to perform their function. That means that they may return results affected by concurrent modifications:
item in lst
lst.index(item)
lst.count(item)
All of the above operations avoid acquiring per-object locks. 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 per-object lock.
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 atomic to other threads:
lst1 + lst2 # concatenates two lists into a new list
x * lst # repeats lst x times into a new list
lst.copy() # returns a shallow copy of the list
The following methods that only operate on a single element with no shifting required are atomic:
lst.append(x) # append to the end of the list, no shifting required
lst.pop() # pop element from the end of the list, no shifting required
The clear() method is also atomic.
Other threads cannot observe elements being removed.
The sort() method is not atomic.
Other threads cannot observe intermediate states during sorting, but the
list appears empty for the duration of the sort.
The following operations may allow lock-free operations to observe intermediate states since they modify multiple elements in place:
lst.insert(idx, item) # shifts elements
lst.pop(idx) # idx not at the end of the list, shifts elements
lst *= x # copies elements in place
The remove() method may allow concurrent modifications since
element comparison may execute arbitrary Python code (via
__eq__()).
extend() is safe to call from multiple threads. However, its
guarantees depend on the iterable passed to it. If it is a list, a
tuple, a set, a frozenset, a dict or a
dictionary view object (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 list (but not its subclasses).
Operations that involve multiple accesses, as well as iteration, are never atomic. For example:
# NOT atomic: read-modify-write
lst[i] = lst[i] + 1
# NOT atomic: check-then-act
if lst:
item = lst.pop()
# NOT thread-safe: iteration while modifying
for item in lst:
process(item) # another thread may modify lst
Consider external synchronization when sharing list instances
across threads.
Thread safety for dict objects¶
Creating a dictionary with the dict constructor is atomic when the
argument to it is a dict or a tuple. When using the
dict.fromkeys() method, dictionary creation is atomic when the
argument is a dict, tuple, set or
frozenset.
The following operations and functions are lock-free and atomic.
d[key] # dict.__getitem__
d.get(key) # dict.get
key in d # dict.__contains__
len(d) # dict.__len__
All other operations from here on hold the per-object lock.
Writing or removing a single item is safe to call from multiple threads and will not corrupt the dictionary:
d[key] = value # write
del d[key] # delete
d.pop(key) # remove and return
d.popitem() # remove and return last item
d.setdefault(key, v) # insert if missing
These operations may compare keys using __eq__(), which can
execute arbitrary Python code. During such comparisons, the dictionary may
be modified by another thread. For built-in types like str,
int, and float, 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 per-object lock for the duration of the operation:
d.copy() # returns a shallow copy of the dictionary
d | other # merges two dicts into a new dict
d.keys() # returns a new dict_keys view object
d.values() # returns a new dict_values view object
d.items() # returns a new dict_items view object
The clear() method holds the lock for its duration. Other
threads cannot observe elements being removed.
The following operations lock both dictionaries. For update()
and |=, this applies only when the other operand is a dict
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 dict
d |= other_dict # both locked when other_dict is a dict
d == other_dict # both locked for dict and subclasses
All comparison operations also compare values using __eq__(),
so for non-built-in types the lock may be released during comparison.
fromkeys() locks both the new dictionary and the iterable
when the iterable is exactly a dict, set, or
frozenset (not subclasses):
dict.fromkeys(a_dict) # locks both
dict.fromkeys(a_set) # locks both
dict.fromkeys(a_frozenset) # locks both
When 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 locked
d |= iterable # iterable is not a dict: only d locked
dict.fromkeys(iterable) # iterable is not a dict/set/frozenset: only result locked
Operations that involve multiple accesses, as well as iteration, are never atomic:
# NOT atomic: read-modify-write
d[key] = d[key] + 1
# NOT atomic: check-then-act (TOCTOU)
if key in d:
del d[key]
# NOT thread-safe: iteration while modifying
for key, value in d.items():
process(key) # another thread may modify d
To avoid time-of-check to time-of-use (TOCTOU) issues, use atomic operations or handle exceptions:
# Use pop() with default instead of check-then-delete
d.pop(key, None)
# Or handle the exception
try:
del d[key]
except KeyError:
pass
To safely iterate over a dictionary that may be modified by another thread, iterate over a copy:
# Make a copy to iterate safely
for key, value in d.copy().items():
process(key)
Consider external synchronization when sharing dict instances
across threads.