When I first needed to serialize numeric array data into raw bytes — for saving to a file, sending over a network socket, or interfacing with a lower-level system — I came across the array module’s tostring() method. It’s a small but genuinely useful piece of Python’s standard library, though it comes with an important caveat I had to learn about: it’s deprecated in favor of tobytes(). In this guide, I want to explain how array-to-string/bytes conversion works, why the API changed, and how I handle this correctly in modern Python.
What Is Python’s array Module?
Before diving into tostring(), it’s worth understanding the array module itself. Unlike a regular Python list, which can hold mixed types and incurs per-element object overhead, the array module provides a compact, type-restricted array — every element must be the same numeric type, stored contiguously in memory, much like an array in C.
import array
int_array = array.array('i', [1, 2, 3, 4, 5])
print(int_array)
Output:
array('i', [1, 2, 3, 4, 5])
The 'i' here is a type code indicating signed integers. Other common type codes include 'b' (signed char), 'f' (float), 'd' (double), and 'u' (Unicode character, though this was removed in Python 3.16 — I’ll touch on this later).
The tostring() Method (Deprecated)
Historically, array.array objects had a .tostring() method that converted the array’s raw memory contents into a Python bytes object:
import array
my_array = array.array('i', [1, 2, 3])
raw_bytes = my_array.tostring()
print(raw_bytes)
If I run this on a modern Python version, I actually get a DeprecationWarning (and on very recent versions, it has been removed entirely):
DeprecationWarning: tostring() is deprecated. Use tobytes() instead.
Why tostring() Was Deprecated
The name tostring() was always a bit misleading — it doesn’t return a human-readable string like "1 2 3". It returns the raw binary representation of the array’s memory contents as a bytes object. Because Python 3 draws a much stricter distinction between str (text) and bytes (binary data) compared to Python 2 — where str and bytes were essentially the same thing — the name tostring() became confusing and inconsistent with this new type model. The Python core developers introduced tobytes() as a clearer, correctly named replacement, and formally deprecated tostring() starting in Python 3.9, planning its eventual removal.
import array
my_array = array.array('i', [1, 2, 3])
raw_bytes = my_array.tobytes()
print(raw_bytes)
Output:
b'\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00'
Each integer here takes up 4 bytes (on most platforms, for the 'i' type code), stored in the machine’s native byte order (little-endian on most modern systems), which is exactly why I see three groups of 4 bytes for the three integers 1, 2, and 3.
Converting Bytes Back to an Array with frombytes()
The reverse operation, frombytes() (replacing the old, similarly deprecated fromstring()), lets me reconstruct an array from raw bytes:
import array
raw_bytes = b'\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00'
restored = array.array('i')
restored.frombytes(raw_bytes)
print(restored)
Output:
array('i', [1, 2, 3])
Getting an Actual Human-Readable String Representation
If what I actually want is a readable string — not raw binary data — I use a completely different approach: converting each element to a string and joining them, or using str()/repr() on the array directly.
import array
my_array = array.array('i', [1, 2, 3, 4, 5])
# Human-readable representation of the array object itself
print(str(my_array))
# Joining elements into a readable, comma-separated string
readable = ", ".join(str(x) for x in my_array)
print(readable)
Output:
array('i', [1, 2, 3, 4, 5])
1, 2, 3, 4, 5
This distinction — raw byte serialization versus a human-readable string — is genuinely important, and mixing them up is one of the most common points of confusion I’ve seen among people learning about the array module.
Internal Working: What tobytes()/tostring() Actually Does
Under the hood, an array.array object stores its elements in a single contiguous block of memory, exactly matching the C-level representation of that type (e.g., a C int for type code 'i', a C double for type code 'd'). Calling tobytes() essentially performs a raw memory copy of that buffer into a Python bytes object — there’s no per-element conversion loop happening at the Python level, which is why this operation is extremely fast, even for large arrays.
import array
import time
big_array = array.array('i', range(1_000_000))
start = time.time()
data = big_array.tobytes()
print(f"Time: {time.time() - start:.6f}s")
print(f"Size in bytes: {len(data)}")
Because it’s essentially copying a raw memory buffer, tobytes() runs in O(n) time relative to the array’s size, but with a very small constant factor compared to any Python-level element-by-element conversion.
Comparing array with NumPy for Serialization
For heavier numerical work, I often use NumPy instead of the standard library’s array module, and NumPy provides its own equivalent methods:
import numpy as np
numpy_array = np.array([1, 2, 3, 4, 5], dtype=np.int32)
raw_bytes = numpy_array.tobytes()
print(raw_bytes)
restored = np.frombuffer(raw_bytes, dtype=np.int32)
print(restored)
Output:
b'\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00\x05\x00\x00\x00'
[1 2 3 4 5]
NumPy’s tobytes() behaves conceptually the same way as array.array.tobytes() — a raw memory dump — but NumPy adds far more functionality around data types, multidimensional shapes, and numerical operations, which is why I default to it for any serious numerical or scientific work.
Real-World and Practical Use Cases
- Binary file formats: writing raw numeric data to a custom binary file format for compact storage.
import array
data = array.array('d', [3.14, 2.71, 1.41])
with open("data.bin", "wb") as f:
f.write(data.tobytes())
with open("data.bin", "rb") as f:
restored = array.array('d')
restored.frombytes(f.read())
print(restored)
Output:
array('d', [3.14, 2.71, 1.41])
- Network communication: sending compact binary representations of numeric data over sockets, rather than verbose text-based formats like JSON, when bandwidth or speed really matters.
- Interfacing with C extensions or hardware: raw byte buffers are often the expected format when interacting with lower-level system APIs.
- Audio/signal processing: raw PCM audio data is frequently manipulated as byte buffers converted to/from arrays of integers or floats.
Common Mistakes and Debugging Tips
- Confusing
tostring()/tobytes()with a human-readable string conversion. These methods return raw binary data, not something meant to be printed or read directly as text. - Using the deprecated
tostring()/fromstring()methods in new code. These raise deprecation warnings in recent Python versions and have been removed entirely in newer releases — always usetobytes()/frombytes()instead. - Mismatched type codes when restoring an array. If I serialize with type code
'i'but restore with'f', the resulting values will be garbage, because the raw bytes are being reinterpreted as a completely different type.
import array
original = array.array('i', [1, 2, 3])
raw = original.tobytes()
wrong_restore = array.array('f')
wrong_restore.frombytes(raw)
print(wrong_restore) # Garbage output, wrong type code used
- Ignoring byte order (endianness) issues when sharing binary data across different machine architectures — for true portability, I should use the
structmodule with explicit byte order specifiers, or NumPy’s more advanced dtype system.
Best Practices I Follow
- Always use
tobytes()/frombytes()instead of the deprecatedtostring()/fromstring(). - Keep track of the exact type code used when serializing, so restoration uses a matching type code.
- For cross-platform or cross-language interoperability, consider the
structmodule for explicit control over byte order and size, or a well-defined serialization format like Protocol Buffers or MessagePack. - Use NumPy for anything beyond simple, one-dimensional numeric serialization needs.
FAQs
Q: Is tostring() still available in the latest Python versions? It was deprecated starting in Python 3.9 and has been removed in more recent versions — tobytes() is the current, correct method.
Q: Does tobytes() work the same way as tostring() did? Yes, functionally identical — it’s purely a rename for clarity, with no behavioral difference.
Q: Can I convert an array directly to a readable string of numbers? Not with tobytes()/tostring() — use something like ", ".join(str(x) for x in my_array) instead for a human-readable representation.
Q: What’s the difference between the array module and NumPy? The array module is a lightweight, standard-library-only option for simple, one-dimensional typed arrays; NumPy is a much more powerful, third-party library supporting multidimensional arrays, vectorized math, and a far richer type system.
Troubleshooting Tips
- If you see a
DeprecationWarningmentioningtostring(), switch totobytes()— the change is a drop-in rename. - If restored data looks like garbage, verify that the type code used for
frombytes()matches the one used for the originaltobytes()call. - If you need cross-language compatibility, consider explicitly specifying byte order using the
structmodule rather than relying on the platform’s native byte order.
Summary
tostring() taught me an important lesson about API naming and how Python evolves over time — a method name that made sense once became a genuine source of confusion as the language’s type system matured, leading to its replacement with the far clearer tobytes(). Understanding the difference between raw binary serialization and human-readable string conversion has saved me from several subtle bugs, especially when working with files, sockets, or any system that expects exact byte-level data.