Hands-on bytes vs bytearray lab: real ops/s for append/extend/slice/copy and when copy dominates over mutate, benchmarked on Linux localhost for SREs.
4 min read
When does bytes immutability force copies that bytearray avoids? This lab times append / extend / join, one-byte mutation, and 4 KiB copy/slice paths on Linux localhost.
Related links:
Lab honesty (1 Oct 2026 IST): Python 3.13.5. Affiliates: 0. Note: on this CPython, b[:] is the same object as b (bytes_slice_is_same_object=True) — slice is not a defensive copy for bytes.
Verdict up front: bytearray.append ~36M/s vs bytes + one byte ~8.0M (~4.4×). Chunked b + chunk ~338× behind bytearray extend; b"".join ≈ extend (~1.06×). Mutating one byte in a 64 KB buffer: bytearray index assign ~107× a bytes rebuild-via-concat.
| Arm | Pattern |
|---|---|
| bytes concat 1B / 64B | b = b + chunk loop |
b"".join | pre-sized immutable build |
| bytearray append / extend | in-place grow |
| bytes rebuild one byte | b[:i]+b"A"+b[i+1:] on 64 KB |
| bytearray index assign | ba[i] = 65 |
| copy / slice / memoryview | 4 KiB buffers |
Script: lab-evidence/63-bytes-vs-bytearray/results/run_lab.py.
| Arm | ops/s | ns/op |
|---|---|---|
bytearray .append | 35,668,744 | 28.0 |
bytearray .extend(1B) | 28,226,262 | 35.4 |
bytes + 1B | 8,029,517 | 124.5 |
| bytearray extend 64B | 30,601,439 | 32.7 |
b"".join 64B chunks | 28,928,490 | 34.6 |
bytes + 64B | 90,424 | 11059.1 |
| Arm | ops/s | ns/op |
|---|---|---|
| bytearray index assign | 27,161,395 | 36.8 |
| bytes rebuild via concat | 254,905 | 3923.0 |
One-byte edit on 64 KB: rebuild copies ~64 KB each time (~3923 ns) vs in-place assign (~37 ns) — ~107×.
| Arm | ops/s | ns/op |
|---|---|---|
bytes slice b[:] | 29,282,019 | 34.2 |
bytes(...) ctor copy | 15,789,195 | 63.3 |
| memoryview(bytes) | 11,729,737 | 85.3 |
| bytearray slice copy | 10,702,868 | 93.4 |
bytearray(...) ctor | 8,830,548 | 113.2 |
b[:] looking “fast” here is partly because it returns the same object — not a copy. Need isolation? Use bytes(b) / bytearray(b) / memoryview deliberately.
bytes with + in a loop — chunked concat was ~338× slower than extend; join fixes immutable builds.b[:] as a defensive copy — may be identity on CPython bytes.b = b + chunk — join or bytearray instead.| Need | Prefer |
|---|---|
| Build then freeze | bytearray → bytes(ba) once |
| Many in-place edits | bytearray |
| Hashable buffer / const payload | bytes |
| Immutable build of known parts | b"".join |
| Zero-copy window | memoryview |
Evidence: /workspace/lab-evidence/63-bytes-vs-bytearray/results/.
Immutability copies; bytearray mutates. On this box append beat one-byte concat ~4.4×, chunked concat trailed extend by ~338×, and a 64 KB one-byte rebuild trailed index assign by ~107×. Use join when you must stay on bytes; use bytearray when the buffer must change.
Lab evidence
Lab 1 Oct 2026 IST. Python 3.13.5. bytearray append vs bytes concat 1B ~4.4x; extend64 vs concat64 ~338x; index assign vs bytes rebuild ~107x; b''.join ≈ bytearray extend. Affiliates: 0. Evidence: lab-evidence/63-bytes-vs-bytearray/.
Summary1 Oct 20264 min read
Hands-on platform.platform vs os.uname host inventory lab: real ops/s plus cache notes, measured on Linux localhost today in this hands-on lab for SREs.
Summary1 Oct 20264 min read
Hands-on signal SIGUSR1 vs threading.Event wakeup lab: real p50 latency in microseconds, measured on Linux localhost today in this hands-on lab for SREs.
Summary1 Oct 20264 min read