AI Skill Report Card

Assessing Python Dict Thread Safety

A-85·Aug 14, 2026·Source: Web
14 / 15

Rule of thumb:

  • GIL-enabled build: single dict method calls (d[k] = v, d.get(k), d.pop(k), len(d)) are atomic and safe across threads. Multi-step sequences (check-then-act) are NOT safe.
  • Free-threaded build (no-GIL): single dict method calls are still atomic/thread-safe (CPython guarantees internal consistency), but multi-step sequences are still NOT safe without external locking.
Python
# UNSAFE on both builds — race between check and act if key not in d: d[key] = compute() # SAFE — use atomic single-call alternatives d.setdefault(key, compute()) # note: compute() still runs even if key exists # SAFE for actual atomicity guarantee — use a lock for compound logic with lock: if key not in d: d[key] = compute()
Recommendation
Add a concrete example involving free-threaded-specific behavior differences (e.g., a case that genuinely diverges between GIL and no-GIL builds) rather than mostly parallel treatment
14 / 15
  1. Identify the build target. Determine if the code must run correctly under free-threaded CPython (3.13+, Py_GIL_DISABLED), standard GIL builds, or both. Thread-safety guarantees differ subtly.
  2. Classify each dict operation:
    • Single atomic operation (__getitem__, __setitem__, __delitem__, __contains__, get, pop, setdefault, update with a single dict/iterable argument, len) → safe from corruption in both build types.
    • Compound/check-then-act sequences (read-modify-write across multiple statements, iterating while another thread mutates) → unsafe in both build types.
  3. Check iteration patterns. Iterating over a dict (for k in d, .keys(), .values(), .items()) while another thread mutates it can raise RuntimeError: dictionary changed size during iteration on GIL builds, and can still be unsafe/undefined on free-threaded builds if not otherwise protected. Never assume safe iteration under concurrent mutation.
  4. Add explicit locking for compound operations. Wrap multi-step logic (read-check-write, counters, "get or create" patterns needing side-effect-free guarantees) in a threading.Lock.
  5. Do not rely on the GIL for correctness in future-proof code. Code that "works" only because the GIL serializes bytecode is fragile once run under free-threaded builds — always use explicit synchronization for compound logic.
  6. Verify with docs/version specifics. Confirm behavior against the target CPython version's threadsafety documentation, since guarantees are an evolving part of the free-threaded build's specification.
Recommendation
Include a short section on testing/detecting race conditions (e.g., stress-testing with threads, using sys.settrace or thread sanitizers) to round out completeness
17 / 20

Example 1: Input: counts[word] = counts.get(word, 0) + 1 called concurrently from multiple threads. Output: Unsafe on both build types — this is a read-modify-write sequence spanning two bytecode-level operations (get then __setitem__), so increments can be lost. Fix: wrap in a lock, or use collections.Counter with explicit locking, or restructure with dict.setdefault inside a lock.

Example 2: Input: value = my_dict.get("key") executed from many threads while another thread does my_dict["key"] = new_value. Output: Safe — both are single atomic dict operations. Each thread sees either the old or new value, never a corrupted/partial one, on both GIL and free-threaded builds.

Example 3: Input: for k, v in shared_dict.items(): process(k, v) while another thread calls shared_dict.pop(k) concurrently. Output: Unsafe — may raise RuntimeError (GIL build) or produce inconsistent iteration results (free-threaded build). Fix: iterate over list(shared_dict.items()) snapshot taken under a lock, or hold a lock for the full iteration.

Recommendation
Consider a decision-tree diagram or table summarizing operation safety instead of prose, for faster scanning during code review
  • Prefer single built-in dict method calls over multi-statement patterns when possible.
  • Use threading.Lock (or RLock if reentrancy is needed) around any compound dict logic shared across threads.
  • Take a snapshot copy (dict(d) or list(d.items())) before iterating if concurrent mutation is possible, and take the snapshot while holding the same lock used for mutations.
  • When targeting free-threaded builds specifically, re-verify assumptions against docs.python.org threadsafety docs for the exact CPython version — guarantees have been refined across 3.13/3.14+ releases.
  • Prefer queue.Queue, concurrent.futures, or higher-level synchronization primitives over hand-rolled locking when the access pattern is producer/consumer.
  • Assuming "atomic in CPython with the GIL" automatically means "safe under free-threaded CPython" — verify, don't assume equivalence.
  • Using setdefault(key, expensive_compute()) and forgetting expensive_compute() always executes eagerly, even when the key already exists — wasteful and can have unwanted side effects.
  • Treating dict.update() as fully atomic when its argument is a lazily-evaluated generator/iterable that itself accesses shared mutable state — the update is atomic but the evaluation of its argument may not be.
  • Believing that removing the GIL (free-threaded build) makes dict-heavy code automatically thread-safe without review — it only preserves internal dict consistency, not program-level correctness of compound logic.
0
Grade A-AI Skill Framework
Scorecard
Criteria Breakdown
Quick Start
14/15
Workflow
14/15
Examples
17/20
Completeness
18/20
Format
15/15
Conciseness
14/15