Algorand Python v1 was released today! 🥳
There are quite a few changes and improvements from the developer preview, so things might look a little different to my previous posts.
If you are new to Python on Algorand, I would recommend watching Alessandro's introduction video and reading through the AlgoKit documentation.
Let's explore how we can use iteration in smart contracts to solve some common problems.
Contract Outline
We'll start by importing from algopy
and declaring a TypeAlias
for a dynamic array of unsigned integers:
from typing import TypeAlias
from algopy import ARC4Contract, String, UInt64, arc4, uenumerate, urange
UInt64Array: TypeAlias = arc4.DynamicArray[arc4.UInt64]
class Iteration(ARC4Contract):
"""A contract demonstrating iteration methods in algopy."""
...
Using a type alias here will make our method signatures easier to read.
Sum an Array
To sum an array of numbers, we define an ARC-4 ABI method that accepts a UInt64Array
:
@arc4.abimethod
def sum(self, array: UInt64Array) -> UInt64:
"""Sums an array of numbers.
Args:
array (UInt64Array): The array to sum.
Returns:
UInt64: The sum of the array.
"""
total = UInt64(0)
for n in array:
total += n.native
return total
Looping over the array has exactly the same syntax as iterating over a Python list.
The only difference is how we work with numbers.
Since the array contains arc4.UInt64
encoded numbers, we need to convert each number to the native AVM uint64
type before adding it to the total.
Where supported, the native equivalent of an ARC4 type can be obtained via the
.native
property. It is possible to use native types in an ABI method and the router will automatically encode and decode these types to their ARC4 equivalent.Source: https://algorandfoundation.github.io/puya/lg-arc4.html
Find the First Even Number
To find the first even number in the array:
@arc4.abimethod
def first_even(self, array: UInt64Array) -> UInt64:
"""Returns the first even number in the array.
Defaults to zero.
Args:
array (UInt64Array): The array to search.
Returns:
UInt64: The first even number.
"""
for n in array:
if n.native % 2 == 0:
return n.native
return UInt64(0)
We loop over the array exactly the same as before, and return the first even native
value that we find.
If no even number is found, we return zero.
In algopy
, UInt64()
is the same as UInt64(0)
, but I think the latter is a bit clearer here.
Find the Index of the Last Even Number
Let's make it a bit more difficult.
To find the index of the last even number in the array:
@arc4.abimethod
def last_even_index(self, array: UInt64Array) -> UInt64:
"""Returns the index of the last even number in the array.
Defaults to zero.
Args:
array (UInt64Array): The array to search.
Returns:
UInt64: The index of the last even number.
"""
for i, n in reversed(uenumerate(array)):
if n.native % 2 == 0:
return i
return UInt64(0)
The enumerate
function in Python is extremely handy.
It lets you loop over some iterable, and use both the index and the value found at that index.
Fortunately, algopy
has an equivalent:
uenumerate
is similar to Python’s built-in enumerate function, but for UInt64 numbers; it allows you to loop over a sequence and have an automatic counter.Source: https://algorandfoundation.github.io/puya/lg-control.html
And to iterate backwards, we can use the native Python reversed
function.
Pretty cool!
Repeat an Action
range
is another extremely useful Python function, and again algopy
provides an equivalent for working with UInt64
numbers: urange
.
We can use it to repeat an action n
times (or do much more complicated things!).
Let's use it to concatenate a string with itself:
@arc4.abimethod
def repeat(self, string: arc4.String, times: UInt64) -> String:
"""Repeats a string a number of times.
Args:
string (arc4.String): The string to repeat.
times (UInt64): The number of times to repeat the string.
Returns:
String: The repeated string.
"""
result = String()
for _i in urange(times):
result += string.native
return result
Note that _
is the Python convention for throwaway variables, but it's not supported in algopy
at the moment.
I'm using _i
to denote that the number is not actually used.
Recursion
Finally, let's look at using recursion to calculate Fibonacci numbers.
It should be noted that recursion is usually inefficient, so it's probably not the best thing to do in a smart contract unless the compiler supports tail call elimination or other optimisation techniques.
But sometimes it is the nicest way to express an idea:
@arc4.abimethod
def fibonacci(self, n: UInt64) -> UInt64:
"""Returns the nth Fibonacci number.
Args:
n (UInt64): The index of the Fibonacci number to return.
Returns:
UInt64: The nth Fibonacci number.
"""
return n if n <= 1 else self.fibonacci(n - 1) + self.fibonacci(n - 2)
Calling this method with any number > 8 will run out of opcodes.
We could increase the opcode budget, but this will do for now.
The Complete Contract Code
The code below is also available on GitHub:
from typing import TypeAlias
from algopy import ARC4Contract, String, UInt64, arc4, uenumerate, urange
UInt64Array: TypeAlias = arc4.DynamicArray[arc4.UInt64]
class Iteration(ARC4Contract):
"""A contract demonstrating iteration methods in algopy."""
@arc4.abimethod
def sum(self, array: UInt64Array) -> UInt64:
"""Sums an array of numbers.
Args:
array (UInt64Array): The array to sum.
Returns:
UInt64: The sum of the array.
"""
total = UInt64(0)
for n in array:
total += n.native
return total
@arc4.abimethod
def first_even(self, array: UInt64Array) -> UInt64:
"""Returns the first even number in the array.
Defaults to zero.
Args:
array (UInt64Array): The array to search.
Returns:
UInt64: The first even number.
"""
for n in array:
if n.native % 2 == 0:
return n.native
return UInt64(0)
@arc4.abimethod
def last_even_index(self, array: UInt64Array) -> UInt64:
"""Returns the index of the last even number in the array.
Defaults to zero.
Args:
array (UInt64Array): The array to search.
Returns:
UInt64: The index of the last even number.
"""
for i, n in reversed(uenumerate(array)):
if n.native % 2 == 0:
return i
return UInt64(0)
@arc4.abimethod
def repeat(self, string: arc4.String, times: UInt64) -> String:
"""Repeats a string a number of times.
Args:
string (arc4.String): The string to repeat.
times (UInt64): The number of times to repeat the string.
Returns:
String: The repeated string.
"""
result = String()
for _i in urange(times):
result += string.native
return result
@arc4.abimethod
def fibonacci(self, n: UInt64) -> UInt64:
"""Returns the nth Fibonacci number.
Args:
n (UInt64): The index of the Fibonacci number to return.
Returns:
UInt64: The nth Fibonacci number.
"""
return n if n <= 1 else self.fibonacci(n - 1) + self.fibonacci(n - 2)
Writing Tests
Tests can be written in pytest.
We can use all the nice features of the library, including fixtures and test parameterisation.
You will need to have a LocalNet instance running in order to execute these.
The code below is also available on GitHub:
import algokit_utils
import pytest
from algokit_utils import get_localnet_default_account
from algokit_utils.config import config
from algosdk.v2client.algod import AlgodClient
from algosdk.v2client.indexer import IndexerClient
from smart_contracts.artifacts.iteration.client import IterationClient
@pytest.fixture(scope="session")
def iteration_client(algod_client: AlgodClient, indexer_client: IndexerClient) -> IterationClient:
config.configure(
debug=True,
# trace_all=True,
)
client = IterationClient(
algod_client,
creator=get_localnet_default_account(algod_client),
indexer_client=indexer_client,
)
client.deploy(
on_schema_break=algokit_utils.OnSchemaBreak.AppendApp,
on_update=algokit_utils.OnUpdate.AppendApp,
)
return client
@pytest.mark.parametrize(
"array, total",
[
([], 0),
([1], 1),
([1, 2, 3, 4, 5], 15),
],
)
def test_sum_array(iteration_client: IterationClient, array: list[int], total: int) -> None:
"""Tests the sum_array() method."""
assert iteration_client.sum(array=array).return_value == total
@pytest.mark.parametrize(
"array, n",
[
([], 0),
([1], 0),
([1, 2, 3, 4, 5], 2),
],
)
def test_first_even(iteration_client: IterationClient, array: list[int], n: int) -> None:
"""Tests the first_even() method."""
assert iteration_client.first_even(array=array).return_value == n
@pytest.mark.parametrize(
"array, index",
[
([], 0),
([1], 0),
([1, 2, 3, 4, 5], 3),
],
)
def test_last_even_index(iteration_client: IterationClient, array: list[int], index: int) -> None:
"""Tests the last_even_index() method."""
assert iteration_client.last_even_index(array=array).return_value == index
@pytest.mark.parametrize(
"string, times, result",
[
("", 0, ""),
("a", 0, ""),
("a", 1, "a"),
("a", 3, "aaa"),
],
)
def test_repeat(iteration_client: IterationClient, string: str, times: int, result: str) -> None:
"""Tests the repeat() method."""
assert iteration_client.repeat(string=string, times=times).return_value == result
@pytest.mark.parametrize(
"n, expected",
[
(0, 0),
(1, 1),
(2, 1),
(3, 2),
(4, 3),
(5, 5),
(6, 8),
(7, 13),
],
)
def test_fibonacci(iteration_client: IterationClient, n: int, expected: int) -> None:
"""Tests the fibonacci() method."""
assert iteration_client.fibonacci(n=n).return_value == expected