AI Skill Report Card

Assessing Python Thread Safety

A-83·Aug 14, 2026·Source: Web
---
name: assessing-python-thread-safety
description: Assesses thread-safety guarantees of Python built-in types and standard library operations, particularly for free-threaded (no-GIL) CPython builds. Use when reviewing concurrent Python code, deciding whether locks are needed around shared data structures, or debugging race conditions involving sets, dicts, lists, or other builtins accessed from multiple threads.
---
14 / 15

When someone asks "is this thread-safe?" for a Python operation, check against these tiers:

  1. Single atomic bytecode operation on a builtin (e.g., d[k] = v, s.add(x), list.append) → thread-safe against corruption in both GIL and free-threaded builds, but not safe for compound logic (check-then-act).
  2. Iteration while another thread mutates the container → NOT safe. Raises RuntimeError: <container> changed size during iteration or produces inconsistent results.
  3. Compound operations (if k not in d: d[k] = v, x = s.pop(); s.add(x), += on shared mutable) → NOT safe. Needs explicit locking regardless of GIL.
  4. Free-threaded builds (3.13+ --disable-gil, 3.16 default consideration) → some historically "safe by GIL accident" patterns are now explicitly documented as unsafe or require the object's internal lock.
Recommendation
Add examples for dict/list/deque specifics rather than only sets, since the skill claims to cover 'other builtins'
14 / 15

Progress:

  • Identify the exact operation(s) and container type in question
  • Classify: single atomic op vs. compound/multi-step
  • Check if code will run under free-threaded CPython (no-GIL) — this changes guarantees
  • Consult the specific object's thread-safety documentation (set, dict, list, deque)
  • Determine if iteration co-occurs with mutation from another thread
  • Recommend lock/no-lock verdict with reasoning
  • If unsafe, suggest fix: threading.Lock, queue.Queue, immutable snapshot, or atomic alternative
Recommendation
Include a concrete before/after code fix example showing lock implementation, not just prose description
  • Atomic, thread-safe (no corruption, GIL or no-GIL):
    • add(), discard(), remove(), pop() — individually
    • Membership test in — individually
    • len() — individually
  • NOT thread-safe:
    • Iterating a set (for x in s) concurrently with any mutation from another thread — may raise RuntimeError or skip/duplicate elements
    • Set comprehensions or set() copy-construction reading from a set being mutated elsewhere
    • Compound patterns: if x in s: s.remove(x) (TOCTOU race)
    • Set algebra operators (|, &, -, ^) and their in-place forms (|=, etc.) when operands are mutated concurrently — these internally iterate
  • Free-threaded build specifics: individual set methods use internal critical sections (per-object locks) to stay atomic, but this does not extend across multiple calls or to iteration — same rules as above, just now enforced without relying on GIL as an implicit lock.

Apply the same tiered reasoning to dict, list, and deque — each has its own doc page under threadsafety.html with per-method atomicity tables; check the specific one rather than assuming set semantics generalize.

16 / 20

Example 1: Input: "Is my_set.add(item) called from multiple threads safe without a lock?" Output: Yes — add() is atomic on both GIL and free-threaded CPython. No corruption risk. If subsequent logic depends on set state after the add (e.g., checking size to trigger an action), that compound check needs a lock.

Example 2: Input:

Python
for item in shared_set: process(item) # meanwhile another thread does shared_set.discard(item)

Output: Unsafe. Iterating while another thread mutates raises RuntimeError: Set changed size during iteration or yields inconsistent traversal. Fix: for item in list(shared_set): ... to snapshot, or wrap both iteration and mutation in the same threading.Lock.

Example 3: Input: if x not in my_set: my_set.add(x) from multiple threads — thread-safe? Output: No. Classic TOCTOU race — two threads can both pass the not in check before either adds, but since add() is idempotent for sets this specific race is often harmless unless you're counting insertions or triggering side effects on "first add". Flag as unsafe for correctness of side effects even though it won't corrupt the set.

Recommendation
Add a decision-tree diagram or summary table for quick lookup across container types instead of prose-only reference section
  • Never assume free-threaded CPython gives the same "GIL accidentally serializes everything" safety net — always check documented atomicity per method.
  • Treat any multi-statement read-modify-write on a shared container as needing a lock, regardless of GIL status.
  • Prefer immutable snapshots (list(s), dict(d)) before iterating if concurrent mutation is possible and exact real-time accuracy isn't required.
  • For producer/consumer patterns, prefer queue.Queue over manually locking a list/set/dict.
  • When in doubt, cite the specific CPython docs page (threadsafety.html) for the container type rather than generalizing from another type's guarantees.
  • Assuming "GIL means my code is thread-safe" — true only for single atomic bytecode-level ops, not for logic spanning multiple operations.
  • Assuming set/dict/list share identical thread-safety tables — check each type individually.
  • Forgetting that iteration itself is a multi-step operation vulnerable to concurrent mutation, even if each individual mutation call is atomic.
  • Over-locking: wrapping every single atomic builtin call in a lock when unnecessary, hurting performance without correctness benefit.
  • Under-locking: leaving compound check-then-act patterns unprotected because "the individual calls are atomic."
0
Grade A-AI Skill Framework
Scorecard
Criteria Breakdown
Quick Start
14/15
Workflow
14/15
Examples
16/20
Completeness
16/20
Format
15/15
Conciseness
14/15