AI Skill Report Card

Assessing Bytearray Thread Safety

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

Under free-threaded CPython (PEP 703, no-GIL builds), individual bytearray method calls are atomic (thread-safe against corruption), but sequences of operations are not atomic. Any code that reads-then-writes, or relies on invariants holding across multiple statements, needs an explicit threading.Lock.

Python
import threading buf = bytearray(1024) lock = threading.Lock() # UNSAFE under free-threading: check-then-act race if len(buf) > 0: buf[0] = 42 # buf could have been resized/cleared by another thread # SAFE: single atomic call buf.append(1) # atomic, no lock needed # SAFE: compound operation needs a lock with lock: if len(buf) > 0: buf[0] = 42
Recommendation
Add an example of a false-positive case—an operation that looks unsafe but is actually fine—to sharpen the classification skill
13 / 15

Progress:

  • Identify every bytearray instance shared across threads
  • Classify each access as single-operation vs. compound (multi-statement)
  • For single operations (single method call, single subscript get/set, append, extend, slice assignment as one statement), no lock needed — CPython guarantees the operation itself won't corrupt internal state
  • For compound operations (read-modify-write, length-check-then-index, iterate-while-mutating, buffer resize interacting with a memoryview), add a threading.Lock or threading.RLock around the whole sequence
  • Pay special attention to memoryview objects backed by a bytearray — resizing the bytearray while a memoryview is exported is unsafe and can raise BufferError or, if not guarded, race
  • Verify on the standard GIL build too — locks are correct there and add negligible overhead, so code should be written lock-safe regardless of build
  • Test with PYTHON_GIL=0 (or a free-threaded build) using stress loops (many threads hammering the same bytearray) to surface races that only appear without the GIL
Recommendation
Include guidance on detecting shared bytearray usage across threads in a large codebase (e.g., grep patterns, static analysis hints) since 'identify every instance' is easier said than done
18 / 20

Example 1: Input:

Python
counter_buf = bytearray(8) def increment(): val = int.from_bytes(counter_buf, "little") val += 1 counter_buf[:] = val.to_bytes(8, "little")

Output: Unsafe — this is a classic read-modify-write race. Two threads can read the same val before either writes back, losing an increment. Fix:

Python
lock = threading.Lock() def increment(): with lock: val = int.from_bytes(counter_buf, "little") val += 1 counter_buf[:] = val.to_bytes(8, "little")

Example 2: Input:

Python
log_buf = bytearray() def append_entry(data: bytes): log_buf.extend(data)

Output: Safe as-is. extend is a single atomic method call; free-threaded CPython guarantees the internal buffer state stays consistent even with concurrent callers. No lock required (though ordering of entries across threads is still nondeterministic — add a lock only if ordering matters, not for safety).

Example 3: Input:

Python
data = bytearray(range(256)) mv = memoryview(data) def resize(): data.extend(b"\x00" * 100) # thread A def read(): print(mv[0]) # thread B

Output: Unsafe. Resizing a bytearray while a memoryview exports it is undefined/racy territory even beyond normal thread-safety — CPython may raise BufferError on the resize if the memoryview is still exported, and concurrent access patterns are unreliable. Guard both the resize and any memoryview-based access with the same lock, or avoid holding long-lived memoryviews over bytearrays that are concurrently resized.

Recommendation
Briefly mention RLock vs Lock tradeoffs with a concrete snippet rather than just naming it in Best Practices, since it's presented as an actionable choice but left unexplained
  • Treat "thread-safe against corruption" and "correct under concurrency" as different things — an atomic op prevents crashes/corrupted memory, not logical races.
  • Default to wrapping any multi-step interaction with a shared bytearray in a lock; only skip the lock when you're confident the operation is a single C-level call.
  • Prefer bytes (immutable) over bytearray when the data doesn't need in-place mutation — it sidesteps thread-safety concerns entirely.
  • When using memoryview on a bytearray, keep the memoryview's lifetime short and lock around any resize (extend, +=, del buf[:], slice assignment that changes length) that happens concurrently with the memoryview's use.
  • Write lock-based code even if you only target the GIL build today — it future-proofs against free-threaded builds and costs little.
  • Assuming "the docs say bytearray operations are thread-safe" means the whole program logic is thread-safe — it only covers individual operations, not sequences.
  • Forgetting that slice assignment with a size change (buf[2:5] = b"longer_replacement") can trigger a resize, which interacts badly with concurrently held memoryviews.
  • Using a plain Lock for read-heavy workloads when an RLock or a threading.Condition-based readers/writer pattern would avoid unnecessary serialization — pick the right primitive for the access pattern, not just "any lock."
  • Testing only on the standard GIL-enabled interpreter and assuming that guarantees correctness on free-threaded builds — the GIL currently masks many races that free-threading exposes.
0
Grade A-AI Skill Framework
Scorecard
Criteria Breakdown
Quick Start
14/15
Workflow
13/15
Examples
18/20
Completeness
13/20
Format
15/15
Conciseness
14/15