AI Skill Report Card

Reasoning About Python List Thread Safety

B+78·Aug 14, 2026·Source: Web
13 / 15

CPython 3.16 guarantees that individual list methods are atomic (thread-safe) with respect to other threads calling list methods on the same list — even under free-threaded builds (--disable-gil). This means:

Python
shared = [] # Safe: each individual call is atomic shared.append(1) # atomic shared.pop() # atomic len(shared) # atomic shared[0] = "x" # atomic # NOT safe: multi-step sequences are not atomic as a whole if shared: # check ... val = shared.pop() # ... then act — race condition between the two

The rule of thumb: one list method call = safe. Multiple calls composed together = not safe, regardless of GIL status.

Recommendation
Verify the CPython 3.16 free-threaded atomicity claims against actual documentation/PEPs — this is a fairly niche and version-specific claim that should be double-checked for accuracy, since being wrong here would mislead users into false confidence.
13 / 15
  1. Identify the operation(s) on the shared list.
  2. Classify as single-call or multi-call:
    • Single call to a list method/operator → covered by CPython's internal thread safety, no lock needed for corruption-avoidance purposes.
    • Sequence of calls implementing a check-then-act, read-modify-write, or iterate-then-mutate pattern → needs external synchronization (a threading.Lock) even though each individual call is safe.
  3. Check for concurrent mutation during iteration. Iterating a list (for x in lst) while another thread mutates it is not guaranteed to raise or behave deterministically — treat as unsafe unless the algorithm tolerates skipped/duplicated elements.
  4. Confirm which guarantee applies:
    • Guarantee is: no memory corruption / crashes / undefined behavior from concurrent single-method calls.
    • Guarantee is NOT: any particular interleaving order, or consistency across multiple calls, or atomicity of user-level compound expressions like lst[i] += 1 (that's __getitem__ + arithmetic + __setitem__ — not atomic).
  5. Add locks where compound operations are needed, and document why (the individual calls being "safe" is a red herring for correctness of the compound operation).

Progress checklist for a code review pass:

  • List every shared list instance touched by more than one thread
  • For each, enumerate the operations performed on it
  • Mark each operation as atomic-safe or compound
  • For compound operations, verify a lock guards the whole sequence
  • For iteration-while-mutating patterns, verify either a lock or a defensive copy (for x in list(shared))
  • Flag +=, -=, and other augmented assignments on list elements/slices as compound
Recommendation
Add a counter-example showing a case that looks unsafe but is actually fine, to sharpen the boundary between the two categories.
17 / 20

Example 1: Input:

Python
counter_list = [0] def worker(): counter_list[0] += 1 # called from many threads

Output: Unsafe. counter_list[0] += 1 desugars to __getitem__(0), add, __setitem__(0, ...) — three separate operations. Each is individually atomic, but the sequence is not, so increments can be lost. Fix: wrap in a threading.Lock, or use collections.Counter with explicit locking, or an itertools/atomic-counter alternative.

Example 2: Input:

Python
queue = [] def producer(): queue.append(item) # many threads def consumer(): if queue: item = queue.pop(0)

Output: append alone is safe. But consumer's check-then-pop is a race: two consumers can both pass if queue: before either pops, and the second pop(0) can raise IndexError or pop unexpected data. Fix: guard with a lock, or use queue.Queue/collections.deque with a condition variable instead of hand-rolled list-based queue.

Example 3: Input:

Python
data = [1, 2, 3, 4, 5] def reader(): for x in data: process(x) def writer(): data.append(6) data.remove(2)

Output: No crash is guaranteed (list internals won't corrupt), but the iteration in reader may see an inconsistent snapshot — skipping or repeating elements — while writer mutates concurrently. If deterministic iteration semantics matter, snapshot first: for x in list(data): process(x), or hold a lock across both the iteration and the mutation.

Recommendation
Consider trimming the Best Practices and Common Pitfalls sections, which have some overlap in content (e.g., augmented assignment and check-then-act appear in both workflow examples and pitfalls) — could consolidate for conciseness.
  • Treat "thread-safe" for CPython lists as "won't segfault or corrupt memory," not "produces correct concurrent programs for free."
  • Prefer higher-level concurrency-safe containers (queue.Queue, collections.deque for FIFO push/pop from both ends, or explicit locks) over relying on raw list for producer/consumer patterns.
  • When reviewing code for the free-threaded build specifically, don't assume old GIL-based reasoning ("only one bytecode runs at a time so my sequence is safe") still holds — the per-method atomicity is now the explicit, documented guarantee, and it does not extend to compound expressions.
  • Document, next to any lock, exactly which invariant it's protecting (e.g., "protects check-then-pop atomicity") so future editors don't remove it thinking single calls are already safe.
  • For slice assignment (lst[a:b] = other) and other single expressions that map to one method call, they are atomic — but confirm it's genuinely one call and not something that expands (e.g., list comprehensions building up a shared list across iterations are not one call).
  • Assuming len(lst) > 0 followed by lst.pop() is safe because both are "thread-safe" — the composition isn't.
  • Assuming augmented assignment (+=, *=) on list elements is atomic — it isn't.
  • Assuming iteration is safe against concurrent mutation just because individual append/remove calls are atomic.
  • Conflating "thread-safe" (no corruption) with "linearizable" or "serializable" (correct outcomes under concurrency) — CPython's guarantee is the former only.
  • Forgetting this guarantee is about the list type specifically; other containers (custom classes, third-party structures) don't inherit it just because they wrap a list internally, unless they add their own synchronization.
0
Grade B+AI Skill Framework
Scorecard
Criteria Breakdown
Quick Start
13/15
Workflow
13/15
Examples
17/20
Completeness
17/20
Format
13/15
Conciseness
13/15