ShopperCove
Menu
All writingBlogTopicsCategoriesAboutRSS
Blog
Categories
Observability & SRE62All categories
About

Plate 72

  1. Blog

bytes vs bytearray: Mutate/Copy Lab

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.

Aditya Challa·30 September 2026·4 min read

Summary
On this page
  1. Intro — what this post promises
  2. Arms
  3. Lab topology
  4. Lead table — build / append (p50)
  5. Mutate — when copy dominates
  6. Copy / slice (4 KiB)
  7. Reading it
  8. Pitfalls
  9. When to pick what
  10. Reproduce
  11. Closing

Intro — what this post promises

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:

  • array vs list ints localhost lab
  • struct.pack vs to_bytes localhost lab
  • string concat vs join localhost lab
  • copy vs deepcopy localhost lab
  • frozenset vs set membership localhost lab
  • contextlib vs try/finally localhost lab
  • enum vs constants localhost lab
  • perf_counter vs time localhost lab

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.


Arms

ArmPattern
bytes concat 1B / 64Bb = b + chunk loop
b"".joinpre-sized immutable build
bytearray append / extendin-place grow
bytes rebuild one byteb[:i]+b"A"+b[i+1:] on 64 KB
bytearray index assignba[i] = 65
copy / slice / memoryview4 KiB buffers

Lab topology

append loops: 20k × 1 byte; chunk loops: 5k × 64 bytes
mutate: 50k edits on 64KB buffer
copy: 20k × 4096-byte buffers
metric: p50 ops/s

Script: lab-evidence/63-bytes-vs-bytearray/results/run_lab.py.


Lead table — build / append (p50)

Armops/sns/op
bytearray .append35,668,74428.0
bytearray .extend(1B)28,226,26235.4
bytes + 1B8,029,517124.5
bytearray extend 64B30,601,43932.7
b"".join 64B chunks28,928,49034.6
bytes + 64B90,42411059.1

Mutate — when copy dominates

Armops/sns/op
bytearray index assign27,161,39536.8
bytes rebuild via concat254,9053923.0

One-byte edit on 64 KB: rebuild copies ~64 KB each time (~3923 ns) vs in-place assign (~37 ns) — ~107×.


Copy / slice (4 KiB)

Armops/sns/op
bytes slice b[:]29,282,01934.2
bytes(...) ctor copy15,789,19563.3
memoryview(bytes)11,729,73785.3
bytearray slice copy10,702,86893.4
bytearray(...) ctor8,830,548113.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.


Reading it

  • Never grow bytes with + in a loop — chunked concat was ~338× slower than extend; join fixes immutable builds.
  • bytearray for mutate-in-place protocols — parsers, checksums, ring buffers.
  • bytes for sharing / hashing / dict keys — immutable and hashable.
  • Copy cost scales with size — the 64 KB rebuild arm is the honesty check.

Pitfalls

  1. b[:] as a defensive copy — may be identity on CPython bytes.
  2. Quadratic b = b + chunk — join or bytearray instead.
  3. Sharing a bytearray across threads/tasks without sync — mutation races.
  4. Converting bytearray→bytes every step — reintroduces copies.

When to pick what

NeedPrefer
Build then freezebytearray → bytes(ba) once
Many in-place editsbytearray
Hashable buffer / const payloadbytes
Immutable build of known partsb"".join
Zero-copy windowmemoryview

Reproduce

python3 lab-evidence/63-bytes-vs-bytearray/results/run_lab.py

Evidence: /workspace/lab-evidence/63-bytes-vs-bytearray/results/.


Closing

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.

bytesbytearrayimmutableappendextendpythonlocalhost labsre

Lab evidence

What I found running this

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/.

Notes when a lab post goes up

Occasional email for new hands-on reviews. No sequence and no sponsors.

Related links

  • Plate 67

    itertools.chain vs Flatten Lab

    30 Sept 2026

  • Plate 17

    platform vs os.uname Inventory: Localhost Lab

    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.

    1 Oct 2026

  • Plate 50

    signal vs threading.Event Wakeup: Localhost Lab

    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.

    1 Oct 2026

On this page

  1. Intro — what this post promises
  2. Arms
  3. Lab topology
  4. Lead table — build / append (p50)
  5. Mutate — when copy dominates
  6. Copy / slice (4 KiB)
  7. Reading it
  8. Pitfalls
  9. When to pick what
  10. Reproduce
  11. Closing
All writingBlogCategoriesTopicsAboutPrivacyRSS

© 2026 ShopperCove