MicroPython ‘bytearray’ Data Type

Contents

Introduction

MicroPython is a subset of the parent Python language and is primarily used on microcontroller boards with constrained processor speeds and memory capacity. This tutorial describes the bytearray data type for the MicroPython port that runs on the BBC micro:bit.

The MicroPython bytearray data type is a container for a sequence of integers in the range of 0 to 255 or binary notation; 0b00000000 to 0b11111111. This means that each element of the sequence is exactly 1 byte (8 bits) in size. The sequence is mutable meaning that it can grow and have its contents changed.

The bytearray object is often used as a buffer for data that's transmitted or received from other devices that are connected to the microcontroller e.g. via a wired serial connection or 2.4GHz WiFi/Bluetooth pairing.

For example a microcontroller might read binary measurements from an intelligent sensor into a bytearray buffer using a synchronous serial protocol such as SPI and I2C. The binary data from the sensor will often require further manipulation by the microcontroller to return a human-readable measurement.

The bytes and bytearray data types are also used as buffers by the methods of the ustruct module. This module is discussed here.

Budding programmers are often confused when first confronted with the bytearray (and bytes) data types. In reality though they are a very simple concept and can be incredibly useful for microcontroller programming as just discussed. The trick is to think of them for what they are: simply a collection of 8-bit bytes. Nothing more, nothing less.

What really can add to the confusion is the way MicroPython displays bytes and bytearrays - this is discussed in detail below.

Basic bytearray Operations

This section will describe how bytearray are displayed, declared, accessed (indexing and slicing) and looped through.

Displaying a bytearray Object

As stated above a bytearray object is a container for a collection of 8-bit bytes. The elements are simply stored as the byte representation in memory. However MicroPython, at first glance, has a somewhat confusing way of displaying (e.g. with the print() function) the object.

A bytearray object is displayed in the following manner:


bytearray(b'string')

Example:
bytearray(b'\x01\nA')
          

Consider the string '\x01\nA' from the example above. It is this that often leads to confusion. However there are a simple set of rules that define the actual byte data the byte string represents.

  1. The string represents a collection of ordered bytes with no spaces or other separators between them. The string '\x01\nA' represents three byte values; \x01, \n and A.
  2. If the byte value is an ascii code for a printable character then that character is displayed e.g. if the value is 65 (decimal) than the character 'A' will display.
  3. MicroPython recognises a small set of special characters known as escape characters. An escape character is prefixed with a backslash (\) followed by a printable character. For example, \n is commonly used in a print() statement to force a newline.

    The list of MicroPython escape characters are described in Table 1. If a bytes element value is the ascii code of one of these escape characters then the escape character representation will be displayed e.g. the byte 12 will display as \f (i.e. a formfeed).

  4. And finally… any remaining value (i.e. neither a printable character or an escape character) is displayed as a hexadecimal value prefixed by \x. Thus a byte value of 143 (decimal) would be display as \x8f.
Table 1: MicroPython Escape Characters
Code Description ascii
\' single quotation 39
\\ Backslash 92
\n New line 10
\r Carriage return 13
\t Tab 9
\b Backspace 8
\f Form feed 12

The following table gives examples of these rules in application. The Byte Values column is the byte value (shown in decimal) that is stored in memory for that element.

String Elements Byte Values
'\x01\nA' \x01, \n, A 1, 10, 65
'\x8f\n\x1d' \x8f, \n, x1d 143, 10, 29
'\x01\x02\x03\x04' \x01, \x02, \x03, \x04 1, 2, 3, 4
'Hello' 'H', 'e', 'l', 'l', 'o' 72, 101, 108, 108, 111

Declaring

A bytearray object is always declared using the bytearray() function.

Syntax


variable_name = bytearray([source[,encoding]])

Where:
variable_name: Name assigned to the bytes object.

source: Optional, one or more values
        that must be able to be evaluated
        to a numerical value between 0..255.

encoding[1]: Optional, never required even if
            a string is passed as a source value.

Examples:
# An empty bytearray object
b1 = bytearray()
⇒ bytearray(b'')

# bytearray object with two elements
# automatically populated with 0's.
b1 = bytearray(2)
⇒ bytearray(b'\x00\x00')

# bytearray object populated from
# a list. May also be a tuple.
bytearray([1, 10, 65])
bytearray((1, 10, 65))
⇒ bytearray(b'\x01\nA')

# bytearray object populated from
# a byte string
 b1 = bytearray(b'\x01\nA')
⇒ bytearray(b'\x01\nA')

# An bytearray populated from a string.
# The encoding argument is optional
bytearray('ABC', 'utf-8')
⇒ bytearray(b'ABC')

# An bytearray populated from a string
# without the  encoding argument
# doesn't causes an error.
b1 = bytearray('ABC')
⇒ bytearray(b'ABC')
          

The main thing to note however when converting a list or tuple to a bytearray is all the elements of the list or tuple must be numerical and should have values between 0 and 255. A negative integer or an integer larger than 255 will not cause an error but will give unexpected results.

Indexing

Each element of a bytearray object can be accessed through its index. Similar to all other MicroPython objects that support indexing (bytes, string, list, tuple, etc.) the first element of a bytearray object has an index of 0 (zero).

The value returned by using an element's index is the true underlying 8-bit byte value, not the byte string representation that is displayed with the (e.g.) print() function. The following example should help clarify this important point.

Example 1

# Examples of accessing elements of a
# bytearray object through their indexes.

b1 = bytearray([10, 50, 90, 140])
print(b1)
print(b1[0],b1[1], b1[2], b1[3], '\n')

print('Accessing elements in reverse order...')
print(b1[-1],b1[-2], b1[-3], b1[-4])

          
Output:

bytearray(b'\n2Z\x8c')
10 50 90 140 

Accessing elements in reverse order...
140 90 50 10
          

This example shows that the index operation returns the actual underlying byte value i.e. 10, 50, 90 140. The byte string displayed by the print(b1) statement looks far more intimidating but is easily broken down and understood by following the rules discussed previously.

Since there are four byte values in the b1 collection, the byte string '\n2Z\x8c' can be broken down into the four components: '\n', '2', 'Z' and '\x8c' where:


'\n' = ascii(10) = 'newline' escape character
'2' = ascii(50)
'Z' = ascii(90)
'\x8c' = 8c (hexadecimal) = 140 (decimal)
          

Example 1 also shows how elements can be accessed in reverse order by using negative indexes.

The bytearray data type is mutable meaning its collection can be extended and its elements updated.

Example 2

# Demonstrates that a bytearray object is
# mutable i.e. its elements can 
# be updated.

l = [1, 2, 3, 4, 5]
b = bytearray(l)
print(l, '\n', b)

# Changing some values
print('First element of b:', b[0])
print('\nAttempting to update b...')
b[0] = 20
print('First element updated:', b[0])
        
Output:

[1, 2, 3, 4, 5] 
 bytearray(b'\x01\x02\x03\x04\x05')
First element of b: 1

Attempting to update b...
First element updated: 20
        

Slicing

To access a specific range of elements inside the bytearray object, the slicing operator, which is a colon ‘:’ is used.

If only a single number is supplied with the slicing operator the first n values of the bytearray will be returned where n is the supplied index. For example b1[:3] will return the first three elements of the bytearray object b1.

If two numbers are supplied with the slicing operator then all elements starting from the first index up to but not including the second index will be returned.

Negative indexes can also be used with the slicing operator allowing access from the end of the bytearray object.

Example 3

# Demonstrates the use of the slicing
# operator ':' on a bytearray.

# Define & populate a bytearray.
l = [9, 5, 9, 2, 4, 8]
b = bytearray(l)
print('l:', l)
print('b:', b)

# Use the slicing operator with one index.
print('b[:3] =', b[:3])

# Use the slicing operator with two indexes.
print('b[2:5] =', b[2:5])

# Use the slicing operator
# with negative indexes.
print('b[-5:-2] =', b[-5:-2])
          
Output:

l: [9, 5, 9, 2, 4, 8]
b: bytearray(b'\t\x05\t\x02\x04\x08')
b[:3] = bytearray(b'\t\x05\t')
b[2:5] = bytearray(b'\t\x02\x04')
b[-5:-2] = bytearray(b'\x05\t\x02')
          

Note: The slicing operation on a bytearray always returns another bytearray which will be a subset of the original object.

Looping through a bytearray

It is a simple process to access each element of a bytearray using a for loop.

Example 4

# Demonstrates how to access each element
# of a bytearray using a 'for' loop.


# Define a bytearray.
l = [9, 5, 9, 2, 4, 8]
b = bytearray(l)
print('l:', l)
print('b:', b)

# looping through the bytearray.
for byte in b:
    print(byte, end=' ')
    
print()
          
Output:

l: [9, 5, 9, 2, 4, 8]
b: bytearray(b'\t\x05\t\x02\x04\x08')
9 5 9 2 4 8
          

Converting Between bytearray and Other Data Types

It is a trivial exercise to convert between bytes, array, list, tuple and set data types using the functions bytes(), array(), list(), tuple() and set() respectively.

One of the conversions in Example 5 is between bytearray and array. Arrays are a very useful collections type and are defined in the uarray module. A tutorial covering MicroPython arrays can be found here.

Example 5

# Demonstrates how to convert between the
# bytearray data type and other MicroPython
# data types.

import uarray

# Define a bytearray with
# a bytes string.
b = bytearray(b'\t\x05\t\x02\x04\x08')
print('bytearray:', b)

# Convert bytearray to bytes
Bytes = bytes(b)
print('bytes:', Bytes)

# Convert bytearray to list
l = list(b)
print('list:', l)

# Convert bytearray to tuple
t = tuple(b)
print('tuple:', t)

# Convert bytearray to set
s = set(b)
print('set:', s)

# Convert bytearray to array
s = uarray.array('B', b)
print('array:', s)

# Convert bytearray to string
B = bytearray('Hello World')
Str = B.decode()
print('\nbytearray:', B)
print('string:', Str)
          
Output:

bytearray: bytearray(b'\t\x05\t\x02\x04\x08')
bytes: b'\t\x05\t\x02\x04\x08'
list: [9, 5, 9, 2, 4, 8]
tuple: (9, 5, 9, 2, 4, 8)
set: {8, 2, 9, 4, 5}
array: array('B', [9, 5, 9, 2, 4, 8])

bytearray: bytearray(b'Hello World')
string: Hello World
          

Note: The above example uses the decode() method to convert a byte string to a string.

bytearray Operators

Table 1: MicroPython bytearray Operators
Operator Description
+

Concatenation; creates a new bytearray object by inserting the second bytearray object to the end of the first.

b1 = bytearray([1, 2])
b2 = bytearray([3, 4])
b1 + b2
⇒ bytearray(b'\x01\x02\x03\x04')

==

Comparison operator tests whether two bytearrays are equal

b1 = bytearray([1, 2])
b2 = bytearray([2, 1])
b3 = bytearray([1, 2])

(b1 == b3) ⇒ True
(b1 == b2) ⇒ False

!=

Comparison operator tests whether two bytearrays are not equal

b1 = bytearray([1, 2])
b2 = bytearray([2, 1])
b3 = bytearray([1, 2])

(b1 != b3) ⇒ False
(b1 != b2) ⇒ True

in

Membership operator

b1 = bytearray([1, 2, 3, 4, 5])
b2 = bytearray([2, 3, 4])
b3 = bytearray([2, 3, 5])
b4 = bytearray([4, 5, 6])

b2 in b1 ⇒ True
b3 in b1 ⇒ False
b4 in b1 ⇒ False

not in

Membership operator

b1 = bytearray([1, 2, 3, 4, 5])
b2 = bytearray([2, 3, 4])
b3 = bytearray([2, 3, 5])
b4 = bytearray([4, 5, 6])

b2 not in b1 ⇒ False
b3 not in b1 ⇒ True
b4 not in b1 ⇒ True

bytearray Functions

Table 2: MicroPython bytearray Functions
Function Description
len(bytes)

Returns the number of elements in a bytearray

b = bytearray([2, 4, 1, 5, 3])
len(b) ⇒ 5

max(bytes)

Returns the maximum value in a bytearray.

b = bytearray([2, 4, 1, 5, 3])
max(b) ⇒ 5

min(array)

Returns the minimum value in a bytearray.

b = bytearray([2, 4, 1, 5, 3])
min(b) ⇒ 1

sorted(array
      [, reverse =
         True|False])

Returns a sorted list of the elements from the bytearray.

b = bytearray([2, 4, 1, 5, 3])

sorted(b)
⇒ [1, 2, 3, 4, 5]

sorted(b, reverse = True)
⇒ [5, 4, 3, 2, 1]

bytearray Methods

A method, like a function, is a set of instructions that perform a task. The difference is that a method is associated with an object, while a function is not.” [codecademy.com]

This series on MicroPython discusses classes and methods here.

Methods are invoked using dot notation i.e. bytearray.Method() with the following table providing simple examples. The table provides an exhaustive list of the MicroPython for micro:bit bytearray methods.

Interestingly, the bytearray data type does not share the rich set of methods that its immutable equivalent bytes data type possess - just a very tiny subset.

Table 3: MicroPython bytearray Methods
Method Description
append(item)

Adds an item to the end of a bytearray.

b= bytearray([1, 2])
b.append(4)
print(b) ⇒ bytearray(b'\x01\x02\x04')

extend(object)

Add the elements of an object with buffer protocol to the end of an array. This means that the object must be another bytearray, a bytes object or an array.

b = bytearray([1, 2])
import uarray
a = uarray.array('B', [5, 6])
b.extend(a)
print(b)
⇒ bytearray(b'\x01\x02\x05\x06')

b = bytearray([1, 2])
Byte = bytes([5, 6])
b.extend(Byte)
print(b)
⇒ bytearray(b'\x01\x02\x05\x06')

b1 = bytearray([1, 2])
b2 = bytearray([5, 6])
b1.extend(b2)
print(b1)
⇒ bytearray(b'\x01\x02\x05\x06')

decode()

Converts the elements of a bytearray to a string containing their unicode character equivalents.

b = bytearray([97, 98, 99])
print(b.decode())
⇒ abc