# 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.