AI Skill Report Card

Analyzing Python Thread Safety

A87·Aug 14, 2026·Source: Web
14 / 15

When asked "is this list/dict operation thread-safe?", check:

  1. Is it a single bytecode-level operation (e.g., list.append, dict[key] = val, list.pop())? → Generally atomic under GIL.
  2. Is it a compound read-modify-write expressed across multiple statements (e.g., if key not in d: d[key] = v)? → NOT atomic, needs a lock even under GIL.
  3. Is the code targeting free-threaded CPython (3.13+ --disable-gil / 3.16 default free-threading)? → Re-verify: many single-method atomicity guarantees still hold (CPython uses internal per-object locks for these), but interleaving of separate method calls is never guaranteed.
Python
import threading shared = [] lock = threading.Lock() def worker(): shared.append(1) # atomic, no lock needed (single op) with lock: # compound op, lock required if shared: shared.pop()
Recommendation
Add a dict/set-specific atomicity reference section, since the current reference focuses heavily on lists
13 / 15

Progress:

  • Identify the container type (list, dict, set, deque, etc.)
  • Identify whether the operation is a single built-in method call or a multi-step sequence
  • Check if it's a mutating op (append, extend, pop, insert, remove, sort) vs. just iteration
  • Determine execution mode: GIL-enabled CPython vs free-threaded build
  • Classify: atomic (safe standalone) / non-atomic (needs lock) / undefined (avoid pattern)
  • If iterating while another thread mutates, flag as unsafe regardless of build
  • Recommend fix: lock, queue.Queue, copy-before-iterate, or immutable snapshot
Recommendation
Include an example showing a 'bad outcome' where a suggested fix is itself flawed, to sharpen judgment on edge cases

Atomic (safe from a single call, no torn state):

  • list.append(x), list.pop(), list.pop(i)
  • list[i] = x, del list[i]
  • list.extend(iterable) — atomic as a whole op, but if iterable is itself being mutated concurrently, behavior is undefined
  • x in list, list[i] (read)

NOT atomic / unsafe to assume ordering:

  • list.sort() — atomic w.r.t. not corrupting internal state, but concurrent reads during sort may see partially-sorted data; concurrent mutation during sort raises ValueError/undefined in some cases
  • Iterating a list (for x in lst) while another thread appends/removes — length/index changes mid-iteration can skip or repeat elements; free-threaded builds do NOT make this deterministic
  • Any sequence like lst[i] = lst[i] + 1 (read-modify-write) — race condition even though each half is atomic
  • len(lst) followed by indexed access — TOCTOU race

Free-threaded build specifics (3.13+):

  • Individual list/dict/set methods remain internally consistent (no memory corruption, no crashes) because CPython holds a per-object critical section lock during the call.
  • This does not extend to multi-statement logic — same rules as GIL builds apply for compound operations.
  • Do not assume del/resize during iteration is safe just because it's free-threaded; it can still raise RuntimeError: list changed size during iteration or skip elements.
17 / 20

Example 1: Input: counts[word] = counts.get(word, 0) + 1 shared across threads without a lock. Output: Unsafe — this is a read-modify-write compound operation. .get() and the assignment are two separate steps; a race can lose updates. Fix: wrap in with lock: or use collections.Counter with an external lock, or use multiprocessing/concurrent.futures with per-thread local counters merged at the end.

Example 2: Input: Thread A does my_list.append(item); Thread B does len(my_list) concurrently, no lock. Output: Safe. Both are single atomic operations on the list; no corruption or crash. B may see the list before or after A's append, but never a torn/partial state.

Example 3: Input: Thread A iterates for x in shared_list: while Thread B does shared_list.remove(x). Output: Unsafe regardless of build — mutating a list during iteration in another thread can cause skipped elements or RuntimeError. Fix: iterate over list(shared_list) (a snapshot copy) or use a lock around the whole iteration+mutation region.

Recommendation
Add a brief note on how to detect these patterns via tooling (e.g., static analysis or grep patterns) for faster triage in large codebases
  • Prefer high-level concurrency primitives (queue.Queue, concurrent.futures, threading.Lock) over relying on low-level atomicity guarantees.
  • Treat "atomic" as meaning "won't corrupt memory," not "produces the logically correct result" — logical correctness still requires locks for compound operations.
  • When reviewing code for the free-threaded build, don't assume behavior identical to GIL builds beyond single-call atomicity; re-audit any code that relied on the GIL as an implicit global lock across multiple statements.
  • Snapshot before iterating if concurrent mutation is possible: for x in list(shared): ....
  • Assuming "the GIL makes everything thread-safe" — it only guarantees atomicity of individual bytecode-level operations, not sequences of them.
  • Assuming free-threaded CPython removes the need for locks — it changes performance characteristics, not the need for correctness locking on compound logic.
  • Using if key not in d: d[key] = ... without a lock ("check-then-act" race).
  • Iterating and mutating the same container from different threads without a lock or a copy.
0
Grade AAI Skill Framework
Scorecard
Criteria Breakdown
Quick Start
14/15
Workflow
13/15
Examples
17/20
Completeness
18/20
Format
14/15
Conciseness
14/15