Data structures
In terms of data structures, Algorand Python currently provides support for composite data types and arrays.
In a restricted and costly computing environment such as a blockchain application, making the correct choice for data structures is crucial.
All ARC-4 data types are supported, and initially were the only choice of data structures in Algorand Python 1.0, other than statically sized native Python tuples. However, ARC-4 encoding is not an efficient encoding for mutations, additionally they were restricted in that they could only contain other ARC-4 types.
As of Algorand Python 2.7, two new array types were introduced algopy.Array, a mutable array type
that supports statically sized native and ARC-4 elements and algopy.ImmutableArray that has
an immutable API and supports dynamically sized native and ARC-4 elements.
Mutability vs Immutability
Section titled “Mutability vs Immutability”A value with an immutable type cannot be modified. Some examples are UInt64, Bytes, tuple and typing.NamedTuple.
Aggregate immutable types such as tuple or ImmutableArray provide a way to produce modified values,
this is done by returning a copy of the original value with the specified changes applied
e.g.
import typingimport algopy
# update a named tuple with _replaceclass MyTuple(typing.NamedTuple): foo: algopy.UInt64 bar: algopy.String
tup1 = MyTuple(foo=algopy.UInt64(12), bar=algopy.String("Hello"))# this does not modify tup1tup2 = tup1._replace(foo=algopy.UInt64(34))
assert tup1.foo != tup2.foo
# update immutable array by appending and reassigningarr = algopy.ImmutableArray[MyTuple]()arr = arr.append(tup1)arr = arr.append(tup2)Mutable types allow direct modification of a value and all references to this value are able to observe the change e.g.
import algopy
# both my_arr and my_arr2 both point to the same arraymy_arr = algopy.Array[algopy.UInt64]()my_arr2 = my_arr
my_arr.append(algopy.UInt64(12))assert my_arr.length == 1assert my_arr2.length == 1
my_arr2.append(algopy.UInt64(34))assert my_arr2.length == 2assert my_arr.length == 2Static size vs Dynamic size
Section titled “Static size vs Dynamic size”A static sized type is a type where its total size in memory is determinable at compile time, for example
UInt64 is always 8 bytes of memory. Aggregate types such as tuple, typing.NamedTuple,
arc4.Struct and arc4.Tuple are static size if all their members are also static size
e.g.
tuple[UInt64, UInt64] is static size as it contains two static sized members.
Any type where its size is not statically defined is dynamically sized e.g. Bytes,
String, tuple[UInt64, String] and Array[UInt64] are all dynamically sized.
Size constraints
Section titled “Size constraints”All bytes on the AVM stack cannot exceed 4096 bytes in length, this means all arrays and structs cannot exceed this size.
Boxes are an exception to this, the contents of a box can be up to 32k bytes.
However loading this entire box into a variable is not possible as it would exceed the AVM limit of 4096 bytes.
However Puya will support reading and writing parts of a box
import typingfrom algopy import Box, FixedArray, Struct, UInt64, arc4, size_of
class BigStruct(Struct): count: UInt64 # 8 bytes large_array: FixedArray[UInt64, typing.Literal[512]] # 4096 bytes
class Contract(arc4.ARC4Contract):
def __init__(self) -> None: self.box = Box(BigStruct) self.box.create()
@arc4.abimethod() def read_box_fails(self) -> UInt64: assert size_of(BigStruct) == 4104 big_struct = self.box.value # this fails to compile because size_of(BigStruct) assert big_struct.count > 0, ""Algorand Python composite types
Section titled “Algorand Python composite types”This is a regular python tuple
- Immutable
- Members can be of any type
- Most useful as an anonymous type
- Each member is stored on the stack, within a function this makes them quite efficient. However when passing to another function they can require a lot of stack manipulations to order all the members correctly on the stack
typing.NamedTuple
Section titled “typing.NamedTuple”- Immutable
- Members can be of any type
- Members are described by a field name and type
- Modified copies can be made using
._replace - Each member is stored on the stack, within a function this makes them quite efficient. However when passing to another function they can require a lot of stack manipulations to order all the members correctly on the stack
Struct
Section titled “Struct”- Can contain any type except transactions
- Members are described by a field name and type
- Can be immutable if using the
frozenclass option and all members are also immutable - Requires
.copy()when mutable and creating additional references - Encoded as a single ARC-4 value on the stack
arc4.Tuple
Section titled “arc4.Tuple”- Can only contain other ARC-4 types
- Can be immutable if all members are also immutable
- Requires
.copy()when mutable and creating additional references - Encoded as a single ARC-4 value on the stack
arc4.Struct
Section titled “arc4.Struct”- Can only contain other ARC-4 types
- Members are described by a field name and type
- Can be immutable if using the
frozenclass option and all members are also immutable - Requires
.copy()when mutable and creating additional references - Encoded as a single ARC-4 value on the stack
Algorand Python array types
Section titled “Algorand Python array types”algopy.FixedArray
Section titled “algopy.FixedArray”- Can contain any type except transactions
- Can only contain a fixed number of elements
- Most efficient array type
- Requires
.copy()if making additional references to the array or any mutable elements
algopy.Array
Section titled “algopy.Array”- Can contain any type except transactions
- Dynamically sized, efficient for reading (when assembled off-chain). Inefficient to manipulate on-chain
- Requires
.copy()if making additional references to the array or any mutable elements
algopy.ReferenceArray
Section titled “algopy.ReferenceArray”- Mutable, all references see modifications
- Only supports static size immutable types. Note: Supporting mutable elements would have the potential to quickly exhaust scratch slots in a program so for this reason this type is limited to immutable elements only
- May use scratch slots to store the data
- Cannot be put in storage or used in ABI method signatures
- An immutable copy can be made for storage or returning from a contract by using the
freezemethod e.g.
import algopy
class SomeContract(algopy.arc4.ARC4Contract): @algopy.arc4.abimethod() def get_array(self) -> algopy.ImmutableArray[algopy.UInt64]: arr = algopy.ReferenceArray[algopy.UInt64]() # modify arr as required ...
# return immutable copy return arr.freeze()algopy.ImmutableArray
Section titled “algopy.ImmutableArray”- Immutable version of
algopy.Array - Modifications are done by reassigning a modified copy of the original array
- Can only contain immutable types
- Can be put in storage or used in ABI method signatures
algopy.arc4.DynamicArray / algopy.arc4.StaticArray
Section titled “algopy.arc4.DynamicArray / algopy.arc4.StaticArray”- Only supports ARC-4 elements
- Elements often require conversion to native types, use
algopy.Array/algopy.FixedArrayto avoid explict conversions - Dynamically sized types are efficient for reading, but not writing
- Requires
.copy()if making additional references to the array or mutable elements
- Avoid using dynamically sized types as they are less efficient and can obfuscate constraints of the AVM (
algopy.Bytes,algopy.String,algopy.Array,algopy.arc4.DynamicArray,algopy.arc4.DynamicBytesalgopy.arc4.String) - Prefer frozen structs where possible to avoid
.copy()requirements - If a function needs just a few values of a tuple it is more efficient to just pass those members rather than the whole tuple
- For passing composite values between functions there can be different trade-offs in terms of op budget and program size between a tuple or a struct, if this is a concern then test and confirm which suits your contract the best.
- All array types except
algopy.ReferenceArraycan be used in storage and ABI methods, and will be viewed externally (i.e. in ARC-56 definitions) as the equivalent ARC-4 encoded type - Use
algopy.ReferenceArray.freezeto convert the array to analgopy.ImmutableArrayfor storage