Answer:
Back to top
View this page
Toggle Light / Dark / Auto color theme
Toggle table of contents sidebar
Algorand Python
¶
Algorand Python is a partial implementation of the Python programming language that runs on the AVM. It includes a statically typed framework for development of Algorand smart contracts and logic signatures, with Pythonic interfaces to underlying AVM functionality that works with standard Python tooling.
Algorand Python is compiled for execution on the AVM by PuyaPy, an optimising compiler that ensures the resulting AVM bytecode execution semantics that match the given Python code. PuyaPy produces output that is directly compatible with
AlgoKit typed clients
to make deployment and calling easy.
Quick start
¶
The easiest way to use Algorand Python is to instantiate a template with AlgoKit via
algokit
init
-t
python
. This will give you a full development environment with intellisense, linting, automatic formatting, breakpoint debugging, deployment and CI/CD.
Alternatively, if you want to start from scratch you can do the following:
Ensure you have Python 3.12+
Install
AlgoKit CLI
Check you can run the compiler:
algokit
compile
py
-h
Install Algorand Python into your project
poetry
add
algorand-python
Create a contract in a (e.g.)
contract.py
file:
from
algopy
import
ARC4Contract
,
arc4
class
HelloWorldContract
(
ARC4Contract
):
@arc4
.
abimethod
def
hello
(
self
,
name
:
arc4
.
String
)
->
arc4
.
String
:
return
"Hello, "
+
name
Compile the contract:
algokit
compile
py
contract.py
You should now have
HelloWorldContract.approval.teal
and
HelloWorldContract.clear.teal
on the file system!
We generally recommend using ARC-32 and
generated typed clients
to have the most optimal deployment and consumption experience; to do this you need to ask PuyaPy to output an ARC-32 compatible app spec file:
algokit
compile
py
contract.py
--output-arc32
--no-output-teal
You should now have
HelloWorldContract.arc32.json
, which can be generated into a client e.g. using AlgoKit CLI:
algokit
generate
client
HelloWorldContract.arc32.json
--output
client.py
From here you can dive into the
examples
or look at the
documentation
.
Programming with Algorand Python
¶
To get started developing with Algorand Python, please take a look at the
Language Guide
.
Using the PuyaPy compiler
¶
To see detailed guidance for using the PuyaPy compiler, please take a look at the
Compiler guide
.
Next
Language Guide
Copyright © 2024, Algorand Foundation
Made with
Sphinx
and
@pradyunsg
's
Furo


Answer:
Back to top
View this page
Toggle Light / Dark / Auto color theme
Toggle table of contents sidebar
Program structure
¶
An Algorand Python smart contract is defined within a single class. You can extend other
contracts (through inheritance), and also define standalone functions and reference them. This also
works across different Python packages - in other words, you can have a Python library with common
functions and re-use that library across multiple projects!
Modules
¶
Algorand Python modules are files that end in
.py
, as with standard Python. Sub-modules are
supported as well, so you’re free to organise your Algorand Python code however you see fit.
The standard python import rules apply, including
relative vs absolute import
requirements.
A given module can contain zero, one, or many smart contracts and/or logic signatures.
A module can contain
contracts
,
subroutines
,
logic signatures
, and
compile-time constant code and values
.
Typing
¶
Algorand Python code must be fully typed with
type annotations
.
In practice, this mostly means annotating the arguments and return types of all functions.
Subroutines
¶
Subroutines are “internal” or “private” methods to a contract. They can exist as part of a contract
class, or at the module level so they can be used by multiple classes or even across multiple
projects.
You can pass parameters to subroutines and define local variables, both of which automatically get
managed for you with semantics that match Python semantics.
All subroutines must be decorated with
algopy.subroutine
, like so:
def
foo
()
->
None
:
# compiler error: not decorated with subroutine
...
@algopy
.
subroutine
def
bar
()
->
None
:
...
Note
Requiring this decorator serves two key purposes:
You get an understandable error message if you try and use a third party package that wasn’t
built for Algorand Python
It provides for the ability to modify the functions on the fly when running in Python itself, in
a future testing framework.
Argument and return types to a subroutine can be any Algorand Python variable type (except for
some inner transaction types
).
Returning multiple values is allowed, this is annotated in the standard Python way with
tuple
:
@algopy
.
subroutine
def
return_two_things
()
->
tuple
[
algopy
.
UInt64
,
algopy
.
String
]:
...
Keyword only and positional only argument list modifiers are supported:
@algopy
.
subroutine
def
my_method
(
a
:
algopy
.
UInt64
,
/
,
b
:
algopy
.
UInt64
,
*
,
c
:
algopy
.
UInt64
)
->
None
:
...
In this example,
a
can only be passed positionally,
b
can be passed either by position or by
name, and
c
can only be passed by name.
The following argument/return types are not currently supported:
Type unions
Variadic args like
*args
,
**kwargs
Python types such as
int
Default values are not supported
Contract classes
¶
An
Algorand smart contract
consists of two distinct “programs”; an approval program, and a
clear-state program. These are tied together in Algorand Python as a single class.
All contracts must inherit from the base class
algopy.Contract
- either directly or indirectly,
which can include inheriting from
algopy.ARC4Contract
.
The life-cycle of a smart contract matches the semantics of Python classes when you consider
deploying a smart contract as “instantiating” the class. Any calls to that smart contract are made
to that instance of the smart contract, and any state assigned to
self.
will persist across
different invocations (provided the transaction it was a part of succeeds, of course). You can
deploy the same contract class multiple times, each will become a distinct and isolated instance.
Contract classes can optionally implement an
__init__
method, which will be executed exactly
once, on first deployment. This method takes no arguments, but can contain arbitrary code,
including reading directly from the transaction arguments via
Txn
. This makes
it a good place to put common initialisation code, particularly in ARC-4 contracts with multiple
methods that allow for creation.
The contract class body should not contain any logic or variable initialisations, only method
definitions. Forward type declarations are allowed.
Example:
class
MyContract
(
algopy
.
Contract
):
foo
:
algopy
.
UInt64
# okay
bar
=
algopy
.
UInt64
(
1
)
# not allowed
if
True
:
# also not allowed
bar
=
algopy
.
UInt64
(
2
)
Only concrete (ie non-abstract) classes produce output artifacts for deployment. To mark a class
as explicitly abstract, inherit from
abc.ABC
.
Note
The compiler will produce a warning if a Contract class is implicitly abstract, i.e. if any
abstract methods are unimplemented.
For more about inheritance and it’s role in code reuse, see the section
in
Code reuse
Contract class configuration
¶
When defining a contract subclass you can pass configuration options to the
algopy.Contract
base class
per the API documentation
.
Namely you can pass in:
name
- Which will affect the output TEAL file name if there are multiple non-abstract contracts
in the same file and will also be used as the contract name in the ARC-32 application.json instead of the class name.
scratch_slots
- Which allows you to mark a slot ID or range of slot IDs as “off limits” to Puya
so you can manually use them.
state_totals
- Which allows defining what values should be used for global and local uint and bytes storage values
when creating a contract and will appear in ARC-32 app spec.
Full example:
GLOBAL_UINTS
=
3
class
MyContract
(
algopy
.
Contract
,
name
=
"CustomName"
,
scratch_slots
=
[
5
,
25
,
algopy
.
urange
(
110
,
115
)],
state_totals
=
algopy
.
StateTotals
(
local_bytes
=
1
,
local_uints
=
2
,
global_bytes
=
4
,
global_uints
=
GLOBAL_UINTS
),
):
...
Example: Simplest possible
algopy.Contract
implementation
¶
For a non-ARC4 contract, the contract class must implement an
approval_program
and
a
clear_state_program
method.
As an example, this is a valid contract that always approves:
class
Contract
(
algopy
.
Contract
):
def
approval_program
(
self
)
->
bool
:
return
True
def
clear_state_program
(
self
)
->
bool
:
return
True
The return value of these methods can be either a
bool
that indicates whether the transaction
should approve or not, or a
algopy.UInt64
value, where
UInt64(0)
indicates that the transaction
should be rejected and any other value indicates that it should be approved.
Example: Simple call counter
¶
Here is a very simple example contract that maintains a counter of how many times it has
been called (including on create).
class
Counter
(
algopy
.
Contract
):
def
__init__
(
self
)
->
None
:
self
.
counter
=
algopy
.
UInt64
(
0
)
def
approval_program
(
self
)
->
bool
:
match
algopy
.
Txn
.
on_completion
:
case
algopy
.
OnCompleteAction
.
NoOp
:
self
.
increment_counter
()
return
True
case
_
:
# reject all OnCompletionAction's other than NoOp
return
False
def
clear_state_program
(
self
)
->
bool
:
return
True
@algopy
.
subroutine
def
increment_counter
(
self
)
->
None
:
self
.
counter
+=
1
Some things to note:
self.counter
will be stored in the application’s
Global State
.
The return type of
__init__
must be
None
, per standard typed Python.
Any methods other than
__init__
,
approval_program
or
clear_state_program
must be decorated
with
@subroutine
.
Example: Simplest possible
algopy.ARC4Contract
implementation
¶
And here is a valid ARC4 contract:
class
ABIContract
(
algopy
.
ARC4Contract
):
pass
A default
@algopy.arc4.baremethod
that allows contract creation is automatically inserted if no
other public method allows execution on create.
The approval program is always automatically generated, and consists of a router which delegates
based on the transaction application args to the correct public method.
A default
clear_state_program
is implemented which always approves, but this can be overridden.
Example: An ARC4 call counter
¶
import
algopy
class
ARC4Counter
(
algopy
.
ARC4Contract
):
def
__init__
(
self
)
->
None
:
self
.
counter
=
algopy
.
UInt64
(
0
)
@algopy
.
arc4
.
abimethod
(
create
=
"allow"
)
def
invoke
(
self
)
->
algopy
.
arc4
.
UInt64
:
self
.
increment_counter
()
return
algopy
.
arc4
.
UInt64
(
self
.
counter
)
@algopy
.
subroutine
def
increment_counter
(
self
)
->
None
:
self
.
counter
+=
1
This functions very similarly to the
simple example
.
Things to note here:
Since the
invoke
method has
create="allow"
, it can be called both as the method to create the
app and also to invoke it after creation. This also means that no default bare-method create will
be generated, so the only way to create the contract is through this method.
The default options for
abimethod
is to only allow
NoOp
as an on-completion-action, so we
don’t need to check this manually.
The current call count is returned from the
invoke
method.
Every method in an
AR4Contract
except for the optional
__init__
and
clear_state_program
methods must be decorated with one of
algopy.arc4.abimethod
,
alogpy.arc4.baremethod
, or
algopy.subroutine
.
subroutines
won’t be directly callable through the default router.
See the
ARC-4 section
of this language guide for more info on the above.
Logic signatures
¶
Logic signatures on Algorand
are stateless, and consist of a single program. As such, they are implemented as functions in Algorand Python
rather than classes.
@algopy
.
logicsig
def
my_log_sig
()
->
bool
:
...
Similar to
approval_program
or
clear_state_program
methods, the function must take no
arguments, and return either
bool
or
algopy.UInt64
. The meaning is the same: a
True
value or non-zero
UInt64
value indicates success,
False
or
UInt64(0)
indicates failure.
Logic signatures can make use of subroutines that are not nested in contract classes.
Next
Types
Previous
Language Guide
Copyright © 2024, Algorand Foundation
Made with
Sphinx
and
@pradyunsg
's
Furo


Answer:
Back to top
View this page
Toggle Light / Dark / Auto color theme
Toggle table of contents sidebar
Types
¶
Algorand Python exposes a number of types that provide a statically typed representation of the behaviour that is possible on the Algorand Virtual Machine.
Types
AVM types
UInt64
Bytes
String
BigUInt
bool
Account
Asset
Application
Python built-in types
bool
tuple
typing.NamedTuple
None
int, str, bytes, float
Template variables
ARC-4 types
AVM types
¶
The most basic
types on the AVM
are
uint64
and
bytes[]
, representing unsigned 64-bit integers and byte arrays respectively.
These are represented by
UInt64
and
Bytes
in Algorand Python.
There are further “bounded” types supported by the AVM, which are backed by these two simple primitives.
For example,
bigint
represents a variably sized (up to 512-bits), unsigned integer, but is actually
backed by a
bytes[]
. This is represented by
BigUInt
in Algorand Python.
UInt64
¶
algopy.UInt64
represents the underlying AVM
uint64
type.
It supports all the same operators as
int
, except for
/
, you must use
//
for truncating
division instead.
# you can instantiate with an integer literal
num
=
algopy
.
UInt64
(
1
)
# no arguments default to the zero value
zero
=
algopy
.
UInt64
()
# zero is False, any other value is True
assert
not
zero
assert
num
# Like Python's `int`, `UInt64` is immutable, so augmented assignment operators return new values
one
=
num
num
+=
1
assert
one
==
1
assert
num
==
2
# note that once you have a variable of type UInt64, you don't need to type any variables
# derived from that or wrap int literals
num2
=
num
+
200
//
3
Further examples available here
.
Bytes
¶
algopy.Bytes
represents the underlying AVM
bytes[]
type. It is intended
to represent binary data, for UTF-8 it might be preferable to use
String
.
# you can instantiate with a bytes literal
data
=
algopy
.
Bytes
(
b
"abc"
)
# no arguments defaults to an empty value
empty
=
algopy
.
Bytes
()
# empty is False, non-empty is True
assert
data
assert
not
empty
# Like Python's `bytes`, `Bytes` is immutable, augmented assignment operators return new values
abc
=
data
data
+=
b
"def"
assert
abc
==
b
"abc"
assert
data
==
b
"abcdef"
# indexing and slicing are supported, and both return a Bytes
assert
abc
[
0
]
==
b
"a"
assert
data
[:
3
]
==
abc
# check if a bytes sequence occurs within another
assert
abc
in
data
Hint
Indexing a
Bytes
returning a
Bytes
differs from the behaviour of Python’s bytes type, which
returns an
int
.
# you can iterate
for
i
in
abc
:
...
# construct from encoded values
base32_seq
=
algopy
.
Bytes
.
from_base32
(
'74======'
)
base64_seq
=
algopy
.
Bytes
.
from_base64
(
'RkY='
)
hex_seq
=
algopy
.
Bytes
.
from_hex
(
'FF'
)
# binary manipulations ^, &, |, and ~ are supported
data
^=
~
((
base32_seq
&
base64_seq
)
|
hex_seq
)
# access the length via the .length property
assert
abc
.
length
==
3
Note
See
Python builtins
for an explanation of why
len()
isn’t supported.
See a full example
.
String
¶
String
is a special Algorand Python type that represents a UTF8 encoded string.
It’s backed by
Bytes
, which can be accessed through the
.bytes
.
It works similarly to
Bytes
, except that it works with
str
literals rather than
bytes
literals. Additionally, due to a lack of AVM support for unicode data, indexing and length
operations are not currently supported (simply getting the length of a UTF8 string is an
O(N)
operation, which would be quite costly in a smart contract). If you are happy using the length as
the number of bytes, then you can call
.bytes.length
.
# you can instantiate with a string literal
data
=
algopy
.
String
(
"abc"
)
# no arguments defaults to an empty value
empty
=
algopy
.
String
()
# empty is False, non-empty is True
assert
data
assert
not
empty
# Like Python's `str`, `String` is immutable, augmented assignment operators return new values
abc
=
data
data
+=
"def"
assert
abc
==
"abc"
assert
data
==
"abcdef"
# whilst indexing and slicing are not supported, the following tests are:
assert
abc
.
startswith
(
"ab"
)
assert
abc
.
endswith
(
"bc"
)
assert
abc
in
data
# you can also join multiple Strings together with a seperator:
assert
algopy
.
String
(
", "
)
.
join
((
abc
,
abc
))
==
"abc, abc"
# access the underlying bytes
assert
abc
.
bytes
==
b
"abc"
See a full example
.
BigUInt
¶
algopy.BigUInt
represents a variable length (max 512-bit) unsigned integer stored
as
bytes[]
in the AVM.
It supports all the same operators as
int
, except for power (
**
), left and right shift (
<<
and
>>
) and
/
(as with
UInt64
, you must use
//
for truncating division instead).
Note that the op code costs for
bigint
math are an order of magnitude higher than those for
uint64
math. If you just need to handle overflow, take a look at the wide ops such as
addw
,
mulw
, etc - all of which are exposed through the
algopy.op
module.
Another contrast between
bigint
and
uint64
math is that
bigint
math ops don’t immediately
error on overflow - if the result exceeds 512-bits, then you can still access the value via
.bytes
, but any further math operations will fail.
# you can instantiate with an integer literal
num
=
algopy
.
BigUInt
(
1
)
# no arguments default to the zero value
zero
=
algopy
.
BigUInt
()
# zero is False, any other value is True
assert
not
zero
assert
num
# Like Python's `int`, `BigUInt` is immutable, so augmented assignment operators return new values
one
=
num
num
+=
1
assert
one
==
1
assert
num
==
UInt64
(
2
)
# note that once you have a variable of type BigUInt, you don't need to type any variables
# derived from that or wrap int literals
num2
=
num
+
200
//
3
Further examples available here
.
bool
¶
The semantics of the AVM
bool
bounded type exactly match the semantics of Python’s built-in
bool
type
and thus Algorand Python uses the in-built
bool
type from Python.
Per the behaviour in normal Python, Algorand Python automatically converts various types to
bool
when they
appear in statements that expect a
bool
e.g.
if
/
while
/
assert
statements, appear in Boolean expressions
(e.g. next to
and
or
or
keywords) or are explicitly casted to a bool.
The semantics of
not
,
and
and
or
are special
per how these keywords work in Python
(e.g. short circuiting).
a
=
UInt64
(
1
)
b
=
UInt64
(
2
)
c
=
a
or
b
d
=
b
and
a
e
=
self
.
expensive_op
(
UInt64
(
0
))
or
self
.
side_effecting_op
(
UInt64
(
1
))
f
=
self
.
expensive_op
(
UInt64
(
3
))
or
self
.
side_effecting_op
(
UInt64
(
42
))
g
=
self
.
side_effecting_op
(
UInt64
(
0
))
and
self
.
expensive_op
(
UInt64
(
42
))
h
=
self
.
side_effecting_op
(
UInt64
(
2
))
and
self
.
expensive_op
(
UInt64
(
3
))
i
=
a
if
b
<
c
else
d
+
e
if
a
:
log
(
"a is True"
)
Further examples available here
.
Account
¶
Account
represents a logical Account, backed by a
bytes[32]
representing the
bytes of the public key (without the checksum). It has various account related methods that can be called from the type.
Also see
algopy.arc4.Address
if needing to represent the address as a distinct type.
Asset
¶
Asset
represents a logical Asset, backed by a
uint64
ID.
It has various asset related methods that can be called from the type.
Application
¶
Application
represents a logical Application, backed by a
uint64
ID.
It has various application related methods that can be called from the type.
Python built-in types
¶
Unfortunately, the
AVM types
don’t map to standard Python primitives. For instance,
in Python, an
int
is unsigned, and effectively unbounded. A
bytes
similarly is limited only by
the memory available, whereas an AVM
bytes[]
has a maximum length of 4096. In order to both maintain
semantic compatibility and allow for a framework implementation in plain Python that will fail under the
same conditions as when deployed to the AVM, support for Python primitives is limited.
In saying that, there are many places where built-in Python types can be used and over time the places these types can be used are expected to increase.
bool
¶
Per above
Algorand Python has full support for
bool
.
tuple
¶
Python tuples are supported as arguments to subroutines, local variables, return types.
typing.NamedTuple
¶
Python named tuples are also supported using
typing.NamedTuple
.
Note
Default field values and subclassing a NamedTuple are not supported
import
typing
import
algopy
class
Pair
(
typing
.
NamedTuple
):
foo
:
algopy
.
Bytes
bar
:
algopy
.
Bytes
None
¶
None
is not supported as a value, but is supported as a type annotation to indicate a function or subroutine
returns no value.
int, str, bytes, float
¶
The
int
,
str
and
bytes
built-in types are currently only supported as
module-level constants
or literals.
They can be passed as arguments to various Algorand Python methods that support them or
when interacting with certain
AVM types
e.g. adding a number to a
UInt64
.
float
is not supported.
Template variables
¶
Template variables can be used to represent a placeholder for a deploy-time provided
value. This can be declared using the
TemplateVar[TYPE]
type where
TYPE
is the
Algorand Python type that it will be interpreted as.
from
algopy
import
BigUInt
,
Bytes
,
TemplateVar
,
UInt64
,
arc4
from
algopy.arc4
import
UInt512
class
TemplateVariablesContract
(
arc4
.
ARC4Contract
):
@arc4
.
abimethod
()
def
get_bytes
(
self
)
->
Bytes
:
return
TemplateVar
[
Bytes
](
"SOME_BYTES"
)
@arc4
.
abimethod
()
def
get_big_uint
(
self
)
->
UInt512
:
x
=
TemplateVar
[
BigUInt
](
"SOME_BIG_UINT"
)
return
UInt512
(
x
)
@arc4
.
baremethod
(
allow_actions
=
[
"UpdateApplication"
])
def
on_update
(
self
)
->
None
:
assert
TemplateVar
[
bool
](
"UPDATABLE"
)
@arc4
.
baremethod
(
allow_actions
=
[
"DeleteApplication"
])
def
on_delete
(
self
)
->
None
:
assert
TemplateVar
[
UInt64
](
"DELETABLE"
)
The resulting TEAL code that PuyaPy emits has placeholders with
TMPL_{template
variable
name}
that expects either an integer value or an encoded bytes value. This behaviour exactly
matches what
AlgoKit Utils expects
.
For more information look at the API reference for
TemplateVar
.
ARC-4 types
¶
ARC-4 data types are a first class concept in Algorand Python. They can be passed into ARC-4
methods (which will translate to the relevant ARC-4 method signature), passed into subroutines, or
instantiated into local variables. A limited set of operations are exposed on some ARC-4 types, but
often it may make sense to convert the ARC-4 value to a native AVM type, in which case you can use
the
native
property to retrieve the value. Most of the ARC-4 types also allow for mutation e.g.
you can edit values in arrays by index.
Please see the
reference documentation
for the different classes that can
be used to represent ARC-4 values or the
ARC-4 documentation
for more information
about ARC-4.
Next
Control flow structures
Previous
Program structure
Copyright © 2024, Algorand Foundation
Made with
Sphinx
and
@pradyunsg
's
Furo


Answer:
Back to top
View this page
Toggle Light / Dark / Auto color theme
Toggle table of contents sidebar
Control flow structures
¶
Control flow in Algorand Python is similar to standard Python control flow, with support for if statements, while loops, for loops, and match statements.
If statements
¶
If statements work the same as Python. The conditions must be an expression that evaluates to bool, which can include a
String or Uint64
among others.
if
condition
:
# block of code to execute if condition is True
elif
condition2
:
# block of code to execute if condition is False and condition2 is True
else
:
# block of code to execute if condition and condition2 are both False
See full example
.
Ternary conditions
¶
Ternary conditions work the same as Python. The condition must be an expression that evaluates to bool, which can include a
String or Uint64
among others.
value1
=
UInt64
(
5
)
value2
=
String
(
">6"
)
if
value1
>
6
else
String
(
"<=6"
)
While loops
¶
While loops work the same as Python. The condition must be an expression that evaluates to bool, which can include a
String or Uint64
among others.
You can use
break
and
continue
.
while
condition
:
# block of code to execute if condition is True
See full example
.
For Loops
¶
For loops are used to iterate over sequences, ranges and
ARC-4 arrays
. They work the same as Python.
Algorand Python provides functions like
uenumerate
and
urange
to facilitate creating sequences and ranges; in-built Python
reversed
method works with these.
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.
urange
is a function that generates a sequence of Uint64 numbers, which you can iterate over.
reversed
returns a reversed iterator of a sequence.
Here is an example of how you can use these functions in a contract:
test_array
=
arc4
.
StaticArray
(
arc4
.
UInt8
(),
arc4
.
UInt8
(),
arc4
.
UInt8
(),
arc4
.
UInt8
())
# urange: reversed items, forward index
for
index
,
item
in
uenumerate
(
reversed
(
urange
(
4
))):
test_array
[
index
]
=
arc4
.
UInt8
(
item
)
assert
test_array
.
bytes
==
Bytes
.
from_hex
(
"03020100"
)
See full
examples
.
Match Statements
¶
Match statements work the same as Python and work for […]
match
value
:
case
pattern1
:
# block of code to execute if pattern1 matches
case
pattern2
:
# block of code to execute if pattern2 matches
case
_
:
# Fallback
Note: Captures and patterns are not supported. Currently, there is only support for basic case/switch functionality; pattern matching and guard clauses are not currently supported.
See full example
.
Next
Module level constructs
Previous
Types
Copyright © 2024, Algorand Foundation
Made with
Sphinx
and
@pradyunsg
's
Furo


Answer:
Back to top
View this page
Toggle Light / Dark / Auto color theme
Toggle table of contents sidebar
Module level constructs
¶
You can write compile-time constant code at a module level and then use them in place of
Python built-in literal types
.
For a full example of what syntax is currently possible see the
test case example
.
Module constants
¶
Module constants are compile-time constant, and can contain
bool
,
int
,
str
and
bytes
.
You can use fstrings and other compile-time constant values in module constants too.
For example:
from
algopy
import
UInt64
,
subroutine
SCALE
=
100000
SCALED_PI
=
314159
@subroutine
def
circle_area
(
radius
:
UInt64
)
->
UInt64
:
scaled_result
=
SCALED_PI
*
radius
**
2
result
=
scaled_result
//
SCALE
return
result
@subroutine
def
circle_area_100
()
->
UInt64
:
return
circle_area
(
UInt64
(
100
))
If statements
¶
You can use if statements with compile-time constants in module constants.
For example:
FOO
=
42
if
FOO
>
12
:
BAR
=
123
else
:
BAR
=
456
Integer math
¶
Module constants can also be defined using common integer expressions.
For example:
SEVEN
=
7
TEN
=
7
+
3
FORTY_NINE
=
7
**
2
Strings
¶
Module
str
constants can use f-string formatting and other common string expressions.
For example:
NAME
=
"There"
MY_FORMATTED_STRING
=
f
"Hello
{
NAME
}
"
# Hello There
PADDED
=
f
"
{
123
:
05
}
"
# "00123"
DUPLICATED
=
"5"
*
3
# "555"
Type aliases
¶
You can create type aliases to make your contract terser and more expressive.
For example:
import
typing
from
algopy
import
arc4
VoteIndexArray
:
typing
.
TypeAlias
=
arc4
.
DynamicArray
[
arc4
.
UInt8
]
Row
:
typing
.
TypeAlias
=
arc4
.
StaticArray
[
arc4
.
UInt8
,
typing
.
Literal
[
3
]]
Game
:
typing
.
TypeAlias
=
arc4
.
StaticArray
[
Row
,
typing
.
Literal
[
3
]]
Move
:
typing
.
TypeAlias
=
tuple
[
arc4
.
UInt64
,
arc4
.
UInt64
]
Bytes32
:
typing
.
TypeAlias
=
arc4
.
StaticArray
[
arc4
.
Byte
,
typing
.
Literal
[
32
]]
Proof
:
typing
.
TypeAlias
=
arc4
.
DynamicArray
[
Bytes32
]
Next
Python builtins
Previous
Control flow structures
Copyright © 2024, Algorand Foundation
Made with
Sphinx
and
@pradyunsg
's
Furo


Answer:
Back to top
View this page
Toggle Light / Dark / Auto color theme
Toggle table of contents sidebar
Python builtins
¶
Some common python builtins have equivalent
algopy
versions, that use an
UInt64
instead of a native
int
.
len
¶
The
len()
builtin is not supported, instead
algopy
types that have a length have a
.length
property of type
UInt64
. This is primarily
due to
len()
always returning
int
and the CPython implementation enforcing that it returns
exactly
int
.
range
¶
The
range()
builtin has an equivalent
algopy.urange
this behaves the same as the python builtin except that it returns
an iteration of
UInt64
values instead of
int
.
enumerate
¶
The
enumerate()
builtin has an equivalent
algopy.uenumerate
this behaves the same as the python builtin except that it returns
an iteration of
UInt64
index values and the corresponding item.
reversed
¶
The
reversed()
builtin is supported when iterating within a
for
loop and behaves the same as the python builtin.
types
¶
See
here
Next
Error handling and assertions
Previous
Module level constructs
Copyright © 2024, Algorand Foundation
Made with
Sphinx
and
@pradyunsg
's
Furo


Answer:
Back to top
View this page
Toggle Light / Dark / Auto color theme
Toggle table of contents sidebar
Error handling and assertions
¶
In Algorand Python, error handling and assertions play a crucial role in ensuring the correctness and robustness of smart contracts.
Assertions
¶
Assertions allow you to immediately fail a smart contract if a
Boolean statement or value
evaluates to
False
. If an assertion fails, it immediately stops the execution of the contract and marks the call as a failure.
In Algorand Python, you can use the Python built-in
assert
statement to make assertions in your code.
For example:
@subroutine
def
set_value
(
value
:
UInt64
):
assert
value
>
4
,
"Value must be > 4"
Assertion error handling
¶
The (optional) string value provided with an assertion, if provided, will be added as a TEAL comment on the end of the assertion line. This works in concert with default AlgoKit Utils app client behaviour to show a TEAL stack trace of an error and thus show the error message to the caller (when source maps have been loaded).
Explicit failure
¶
For scenarios where you need to fail a contract explicitly, you can use
the
op.err()
operation. This operation causes the TEAL program to immediately
and unconditionally fail.
Alternatively
op.exit(0)
will achieve the same result. A non-zero value will
do the opposite and immediately succeed.
Exception handling
¶
The AVM doesn’t provide error trapping semantics so it’s not possible to implement
raise
and
catch
.
For more details see
Unsupported Python features
.
Next
Storing data on-chain
Previous
Python builtins
Copyright © 2024, Algorand Foundation
Made with
Sphinx
and
@pradyunsg
's
Furo


Answer:
Back to top
View this page
Toggle Light / Dark / Auto color theme
Toggle table of contents sidebar
Storing data on-chain
¶
Algorand smart contracts have
three different types of on-chain storage
they can utilise:
Global storage
,
Local storage
,
Box Storage
, and
Scratch storage
.
The life-cycle of a smart contract matches the semantics of Python classes when you consider
deploying a smart contract as “instantiating” the class. Any calls to that smart contract are made
to that instance of the smart contract, and any state assigned to
self.
variables will persist
across different invocations (provided the transaction it was a part of succeeds, of course). You can
deploy the same contract class multiple times, each will become a distinct and isolated instance.
During a single smart contract execution there is also the ability to use “temporary” storage
either global to the contract execution via
Scratch storage
, or local to
the current method via
local variables and subroutine params
.
Global storage
¶
Global storage is state that is stored against the contract instance and can be retrieved
by key. There are
AVM limits to the amount of global storage that can be allocated to a contract
.
This is represented in Algorand Python by either:
Assigning any
Algorand Python typed
value to an instance variable (e.g.
self.value
=
UInt64(3)
).
Use this approach if you just require a terse API for getting and setting a state value
Using an instance of
GlobalState
, which gives
some extra features
to understand
and control the value and the metadata of it (which propagates to the ARC-32 app spec file)
Use this approach if you need to:
Omit a default/initial value
Delete the stored value
Check if a value exists
Specify the exact key bytes
Include a description to be included in App Spec files (ARC32/ARC56)
For example:
self
.
global_int_full
=
GlobalState
(
UInt64
(
55
),
key
=
"gif"
,
description
=
"Global int full"
)
self
.
global_int_simplified
=
UInt64
(
33
)
self
.
global_int_no_default
=
GlobalState
(
UInt64
)
self
.
global_bytes_full
=
GlobalState
(
Bytes
(
b
"Hello"
))
self
.
global_bytes_simplified
=
Bytes
(
b
"Hello"
)
self
.
global_bytes_no_default
=
GlobalState
(
Bytes
)
global_int_full_set
=
bool
(
self
.
global_int_full
)
bytes_with_default_specified
=
self
.
global_bytes_no_default
.
get
(
b
"Default if no value set"
)
error_if_not_set
=
self
.
global_int_no_default
.
value
These values can be assigned anywhere you have access to
self
i.e. any instance methods/subroutines. The information about
global storage is automatically included in the ARC-32 app spec file and thus will automatically appear within
any
generated typed clients
.
Local storage
¶
Local storage is state that is stored against the contract instance for a specific account and can be retrieved
by key and account address. There are
AVM limits to the amount of local storage that can be allocated to a contract
.
This is represented in Algorand Python by using an instance of
LocalState
.
For example:
def
__init__
(
self
)
->
None
:
self
.
local
=
LocalState
(
Bytes
)
self
.
local_with_metadata
=
LocalState
(
UInt64
,
key
=
"lwm"
,
description
=
"Local with metadata"
)
@subroutine
def
get_guaranteed_data
(
self
,
for_account
:
Account
)
->
Bytes
:
return
self
.
local
[
for_account
]
@subroutine
def
get_data_with_default
(
self
,
for_account
:
Account
,
default
:
Bytes
)
->
Bytes
:
return
self
.
local
.
get
(
for_account
,
default
)
@subroutine
def
get_data_or_assert
(
self
,
for_account
:
Account
)
->
Bytes
:
result
,
exists
=
self
.
local
.
maybe
(
for_account
)
assert
exists
,
"no data for account"
return
result
@subroutine
def
set_data
(
self
,
for_account
:
Account
,
value
:
Bytes
)
->
None
:
self
.
local
[
for_account
]
=
value
@subroutine
def
delete_data
(
self
,
for_account
:
Account
)
->
None
:
del
self
.
local
[
for_account
]
These values can be assigned anywhere you have access to
self
i.e. any instance methods/subroutines. The information about
local storage is automatically included in the ARC-32 app spec file and thus will automatically appear within
any
generated typed clients
.
Box storage
¶
We provide 3 different types for accessing box storage:
Box
,
BoxMap
, and
BoxRef
. We also expose raw operations via the
AVM ops
module.
Before using box storage, be sure to familiarise yourself with the
requirements and restrictions
of the underlying API.
The
Box
type provides an abstraction over storing a single value in a single box. A box can be declared against
self
in an
__init__
method (in which case the key must be a compile time constant); or as a local variable within any
subroutine.
Box
proxy instances can be passed around like any other value.
Once declared, you can interact with the box via its instance methods.
import
typing
as
t
from
algopy
import
Box
,
arc4
,
Contract
,
op
class
MyContract
(
Contract
):
def
__init__
(
self
)
->
None
:
self
.
box_a
=
Box
(
arc4
.
StaticArray
[
arc4
.
UInt32
,
t
.
Literal
[
20
]],
key
=
b
"a"
)
def
approval_program
(
self
)
->
bool
:
box_b
=
Box
(
arc4
.
String
,
key
=
b
"b"
)
box_b
.
value
=
arc4
.
String
(
"Hello"
)
# Check if the box exists
if
self
.
box_a
:
# Reassign the value
self
.
box_a
.
value
[
2
]
=
arc4
.
UInt32
(
40
)
else
:
# Assign a new value
self
.
box_a
.
value
=
arc4
.
StaticArray
[
arc4
.
UInt32
,
t
.
Literal
[
20
]]
.
from_bytes
(
op
.
bzero
(
20
*
4
))
# Read a value
return
self
.
box_a
.
value
[
4
]
==
arc4
.
UInt32
(
2
)
BoxMap
is similar to the
Box
type, but allows for grouping a set of boxes with a common key and content type.
A custom
key_prefix
can optionally be provided, with the default being to use the variable name as the prefix.
The key can be a
Bytes
value, or anything that can be converted to
Bytes
. The final box name is the combination of
key_prefix
+
key
.
from
algopy
import
BoxMap
,
Contract
,
Account
,
Txn
,
String
class
MyContract
(
Contract
):
def
__init__
(
self
)
->
None
:
self
.
my_map
=
BoxMap
(
Account
,
String
,
key_prefix
=
b
"a_"
)
def
approval_program
(
self
)
->
bool
:
# Check if the box exists
if
Txn
.
sender
in
self
.
my_map
:
# Reassign the value
self
.
my_map
[
Txn
.
sender
]
=
String
(
" World"
)
else
:
# Assign a new value
self
.
my_map
[
Txn
.
sender
]
=
String
(
"Hello"
)
# Read a value
return
self
.
my_map
[
Txn
.
sender
]
==
String
(
"Hello World"
)
BoxRef
is a specialised type for interacting with boxes which contain binary data. In addition to being able to set and read the box value, there are operations for extracting and replacing just a portion of the box data which
is useful for minimizing the amount of reads and writes required, but also allows you to interact with byte arrays which are longer than the AVM can support (currently 4096).
from
algopy
import
BoxRef
,
Contract
,
Global
,
Txn
class
MyContract
(
Contract
):
def
approval_program
(
self
)
->
bool
:
my_blob
=
BoxRef
(
key
=
b
"blob"
)
sender_bytes
=
Txn
.
sender
.
bytes
app_address
=
Global
.
current_application_address
.
bytes
assert
my_blob
.
create
(
8000
)
my_blob
.
replace
(
0
,
sender_bytes
)
my_blob
.
splice
(
0
,
0
,
app_address
)
first_64
=
my_blob
.
extract
(
0
,
32
*
2
)
assert
first_64
==
app_address
+
sender_bytes
assert
my_blob
.
delete
()
value
,
exists
=
my_blob
.
maybe
()
assert
not
exists
assert
my_blob
.
get
(
default
=
sender_bytes
)
==
sender_bytes
my_blob
.
create
(
sender_bytes
+
app_address
)
assert
my_blob
,
"Blob exists"
assert
my_blob
.
length
==
64
return
True
If none of these abstractions suit your needs, you can use the box storage
AVM ops
to interact with box storage. These ops match closely to the opcodes available on the AVM.
For example:
op
.
Box
.
create
(
b
"key"
,
size
)
op
.
Box
.
put
(
Txn
.
sender
.
bytes
,
answer_ids
.
bytes
)
(
votes
,
exists
)
=
op
.
Box
.
get
(
Txn
.
sender
.
bytes
)
op
.
Box
.
replace
(
TALLY_BOX_KEY
,
index
,
op
.
itob
(
current_vote
+
1
))
See the
voting contract example
for a real-world example that uses box storage.
Scratch storage
¶
To use stratch storage you need to
register the scratch storage that you want to use
and then you can use the scratch storage
AVM ops
.
For example:
from
algopy
import
Bytes
,
Contract
,
UInt64
,
op
,
urange
TWO
=
2
TWENTY
=
20
class
MyContract
(
Contract
,
scratch_slots
=
(
1
,
TWO
,
urange
(
3
,
TWENTY
))):
def
approval_program
(
self
)
->
bool
:
op
.
Scratch
.
store
(
1
,
UInt64
(
5
))
op
.
Scratch
.
store
(
2
,
Bytes
(
b
"Hello World"
))
for
i
in
urange
(
3
,
20
):
op
.
Scratch
.
store
(
i
,
i
)
assert
op
.
Scratch
.
load_uint64
(
1
)
==
UInt64
(
5
)
assert
op
.
Scratch
.
load_bytes
(
2
)
==
b
"Hello World"
assert
op
.
Scratch
.
load_uint64
(
5
)
==
UInt64
(
5
)
return
True
def
clear_state_program
(
self
)
->
bool
:
return
True
Next
Logging
Previous
Error handling and assertions
Copyright © 2024, Algorand Foundation
Made with
Sphinx
and
@pradyunsg
's
Furo


Answer:
Back to top
View this page
Toggle Light / Dark / Auto color theme
Toggle table of contents sidebar
Logging
¶
Algorand Python provides a
log
method
that allows you to emit debugging and event information as well as return values from your contracts to the caller.
This
log
method is a superset of the
AVM
log
method
that adds extra functionality:
You can log multiple items rather than a single item
Items are concatenated together with an optional separator (which defaults to:
""
)
Items are automatically converted to bytes for you
Support for:
int
literals / module variables (encoded as raw bytes, not ASCII)
UInt64
values (encoded as raw bytes, not ASCII)
str
literals / module variables (encoded as UTF-8)
bytes
literals / module variables (encoded as is)
Bytes
values (encoded as is)
BytesBacked
values, which includes
String
,
BigUInt
,
Account
and all of the
ARC-4 types
(encoded as their underlying bytes values)
Logged values are
available to the calling client
and attached to the transaction record stored on the blockchain ledger.
If you want to emit ARC-28 events in the logs then there is a
purpose-built function for that
.
Here’s an example contract that uses the log method in various ways:
from
algopy
import
BigUInt
,
Bytes
,
Contract
,
log
,
op
class
MyContract
(
Contract
):
def
approval_program
(
self
)
->
bool
:
log
(
0
)
log
(
b
"1"
)
log
(
"2"
)
log
(
op
.
Txn
.
num_app_args
+
3
)
log
(
Bytes
(
b
"4"
)
if
op
.
Txn
.
num_app_args
else
Bytes
())
log
(
b
"5"
,
6
,
op
.
Txn
.
num_app_args
+
7
,
BigUInt
(
8
),
Bytes
(
b
"9"
)
if
op
.
Txn
.
num_app_args
else
Bytes
(),
sep
=
"_"
,
)
return
True
def
clear_state_program
(
self
)
->
bool
:
return
True
Next
Transactions
Previous
Storing data on-chain
Copyright © 2024, Algorand Foundation
Made with
Sphinx
and
@pradyunsg
's
Furo


Answer:
Back to top
View this page
Toggle Light / Dark / Auto color theme
Toggle table of contents sidebar
Transactions
¶
Algorand Python provides types for accessing fields of other transactions in a group, as well as
creating and submitting inner transactions from your smart contract.
The following types are available:
Group Transactions
Inner Transaction Field sets
Inner Transaction
PaymentTransaction
Payment
PaymentInnerTransaction
KeyRegistrationTransaction
KeyRegistration
KeyRegistrationInnerTransaction
AssetConfigTransaction
AssetConfig
AssetConfigInnerTransaction
AssetTransferTransaction
AssetTransfer
AssetTransferInnerTransaction
AssetFreezeTransaction
AssetFreeze
AssetFreezeInnerTransaction
ApplicationCallTransaction
ApplicationCall
ApplicationCallInnerTransaction
Transaction
InnerTransaction
InnerTransactionResult
Group Transactions
¶
Group transactions can be used as ARC4 parameters or instantiated from a group index.
ARC4 parameter
¶
Group transactions can be used as parameters in ARC4 method
For example to require a payment transaction in an ARC4 ABI method:
import
algopy
class
MyContract
(
algopy
.
ARC4Contract
):
@algopy
.
arc4
.
abimethod
()
def
process_payment
(
self
:
algopy
.
gtxn
.
PaymentTransaction
)
->
None
:
...
Group Index
¶
Group transactions can also be created using the group index of the transaction.
If instantiating one of the type specific transactions they will be checked to ensure the transaction is of the expected type.
Transaction
is not checked for a specific type and provides access to all transaction fields
For example, to obtain a reference to a payment transaction:
import
algopy
@algopy
.
subroutine
()
def
process_payment
(
group_index
:
algopy
.
UInt64
)
->
None
:
pay_txn
=
algopy
.
gtxn
.
PaymentTransaction
(
group_index
)
...
Inner Transactions
¶
Inner transactions are defined using the parameter types, and can then be submitted individually by calling the
.submit()
method, or as a group by calling
submit_txns
Examples
¶
Create and submit an inner transaction
¶
from
algopy
import
Account
,
UInt64
,
itxn
,
subroutine
@subroutine
def
example
(
amount
:
UInt64
,
receiver
:
Account
)
->
None
:
itxn
.
Payment
(
amount
=
amount
,
receiver
=
receiver
,
fee
=
0
,
)
.
submit
()
Accessing result of a submitted inner transaction
¶
from
algopy
import
Asset
,
itxn
,
subroutine
@subroutine
def
example
()
->
Asset
:
asset_txn
=
itxn
.
AssetConfig
(
asset_name
=
b
"Puya"
,
unit_name
=
b
"PYA"
,
total
=
1000
,
decimals
=
3
,
fee
=
0
,
)
.
submit
()
return
asset_txn
.
created_asset
Submitting multiple transactions
¶
from
algopy
import
Asset
,
Bytes
,
itxn
,
log
,
subroutine
@subroutine
def
example
()
->
tuple
[
Asset
,
Bytes
]:
asset1_params
=
itxn
.
AssetConfig
(
asset_name
=
b
"Puya"
,
unit_name
=
b
"PYA"
,
total
=
1000
,
decimals
=
3
,
fee
=
0
,
)
app_params
=
itxn
.
ApplicationCall
(
app_id
=
1234
,
app_args
=
(
Bytes
(
b
"arg1"
),
Bytes
(
b
"arg1"
))
)
asset1_txn
,
app_txn
=
itxn
.
submit_txns
(
asset1_params
,
app_params
)
# log some details
log
(
app_txn
.
logs
(
0
))
log
(
asset1_txn
.
txn_id
)
log
(
app_txn
.
txn_id
)
return
asset1_txn
.
created_asset
,
app_txn
.
logs
(
1
)
Create an ARC4 application, and then call it
¶
from
algopy
import
Bytes
,
arc4
,
itxn
,
subroutine
HELLO_WORLD_APPROVAL
:
bytes
=
...
HELLO_WORLD_CLEAR
:
bytes
=
...
@subroutine
def
example
()
->
None
:
# create an application
application_txn
=
itxn
.
ApplicationCall
(
approval_program
=
HELLO_WORLD_APPROVAL
,
clear_state_program
=
HELLO_WORLD_CLEAR
,
fee
=
0
,
)
.
submit
()
app
=
application_txn
.
created_app
# invoke an ABI method
call_txn
=
itxn
.
ApplicationCall
(
app_id
=
app
,
app_args
=
(
arc4
.
arc4_signature
(
"hello(string)string"
),
arc4
.
String
(
"World"
)),
fee
=
0
,
)
.
submit
()
# extract result
hello_world_result
=
arc4
.
String
.
from_log
(
call_txn
.
last_log
)
Create and submit transactions in a loop
¶
from
algopy
import
Account
,
UInt64
,
itxn
,
subroutine
@subroutine
def
example
(
receivers
:
tuple
[
Account
,
Account
,
Account
])
->
None
:
for
receiver
in
receivers
:
itxn
.
Payment
(
amount
=
UInt64
(
1_000_000
),
receiver
=
receiver
,
fee
=
0
,
)
.
submit
()
Limitations
¶
Inner transactions are powerful, but currently do have some restrictions in how they are used.
Inner transaction objects cannot be passed to or returned from subroutines
¶
from
algopy
import
Application
,
Bytes
,
itxn
,
subroutine
@subroutine
def
parameter_not_allowed
(
txn
:
itxn
.
PaymentInnerTransaction
)
->
None
:
# this is a compile error
...
@subroutine
def
return_not_allowed
()
->
itxn
.
PaymentInnerTransaction
:
# this is a compile error
...
@subroutine
def
passing_fields_allowed
()
->
Application
:
txn
=
itxn
.
ApplicationCall
(
...
)
.
submit
()
do_something
(
txn
.
txn_id
,
txn
.
logs
(
0
))
# this is ok
return
txn
.
created_app
# and this is ok
@subroutine
def
do_something
(
txn_id
:
Bytes
):
# this is just a regular subroutine
...
Inner transaction parameters cannot be reassigned without a
.copy()
¶
from
algopy
import
itxn
,
subroutine
@subroutine
def
example
()
->
None
:
payment
=
itxn
.
Payment
(
...
)
reassigned_payment
=
payment
# this is an error
copied_payment
=
payment
.
copy
()
# this is ok
Inner transactions cannot be reassigned
¶
from
algopy
import
itxn
,
subroutine
@subroutine
def
example
()
->
None
:
payment_txn
=
itxn
.
Payment
(
...
)
.
submit
()
reassigned_payment_txn
=
payment_txn
# this is an error
txn_id
=
payment_txn
.
txn_id
# this is ok
Inner transactions methods cannot be called if there is a subsequent inner transaction submitted or another subroutine is called.
¶
from
algopy
import
itxn
,
subroutine
@subroutine
def
example
()
->
None
:
app_1
=
itxn
.
ApplicationCall
(
...
)
.
submit
()
log_from_call1
=
app_1
.
logs
(
0
)
# this is ok
# another inner transaction is submitted
itxn
.
ApplicationCall
(
...
)
.
submit
()
# or another subroutine is called
call_some_other_subroutine
()
app1_txn_id
=
app_1
.
txn_id
# this is ok, properties are still available
another_log_from_call1
=
app_1
.
logs
(
1
)
# this is not allowed as the array results may no longer be available, instead assign to a variable before submitting another transaction
Next
AVM operations
Previous
Logging
Copyright © 2024, Algorand Foundation
Made with
Sphinx
and
@pradyunsg
's
Furo


Answer:
Back to top
View this page
Toggle Light / Dark / Auto color theme
Toggle table of contents sidebar
AVM operations
¶
Algorand Python allows you to do express
every op code the AVM has available
apart from ops that manipulate the stack (to avoid conflicts with the compiler), and
log
(to avoid confusion with the superior
Algorand Python log function
). These ops are exposed via the
algopy.op
submodule.
We generally recommend importing this entire submodule so you can use intellisense to discover the available methods:
from
algopy
import
UInt64
,
op
,
subroutine
@subroutine
def
sqrt_16
()
->
UInt64
:
return
op
.
sqrt
(
16
)
All ops are typed using Algorand Python types and have correct static type representations.
Many ops have higher-order functionality that Algorand Python exposes and would limit the need to reach for the underlying ops. For instance, there is first-class support for local and global storage so there is little need to use the likes of
app_local_get
et. al. But they are still exposed just in case you want to do something that Algorand Python’s abstractions don’t support.
Txn
¶
The
Txn
opcodes are so commonly used they have been exposed directly in the
algopy
module and can be easily imported to make it terser to access:
from
algopy
import
subroutine
,
Txn
@subroutine
def
has_no_app_args
()
->
bool
:
return
Txn
.
num_app_args
==
0
Global
¶
The
Global
opcodes are so commonly used they have been exposed directly in the
algopy
module and can be easily imported to make it terser to access:
from
algopy
import
subroutine
,
Global
,
Txn
@subroutine
def
only_allow_creator
()
->
None
:
assert
Txn
.
sender
==
Global
.
creator_address
,
"Only the contract creator can perform this operation"
Next
Opcode budgets
Previous
Transactions
Copyright © 2024, Algorand Foundation
Made with
Sphinx
and
@pradyunsg
's
Furo


Answer:
Back to top
View this page
Toggle Light / Dark / Auto color theme
Toggle table of contents sidebar
Opcode budgets
¶
Algorand Python provides a helper method for increasing the
available opcode budget
, see
algopy.ensure_budget
.
Next
ARC-4: Application Binary Interface
Previous
AVM operations
Copyright © 2024, Algorand Foundation
Made with
Sphinx
and
@pradyunsg
's
Furo


Answer:
Back to top
View this page
Toggle Light / Dark / Auto color theme
Toggle table of contents sidebar
ARC-4: Application Binary Interface
¶
ARC4
defines a set of encodings and behaviors for authoring and interacting with an Algorand Smart Contract. It is not the only way to author a smart contract, but adhering to it will make it easier for other clients and users to interop with your contract.
To author an arc4 contract you should extend the
ARC4Contract
base class.
from
algopy
import
ARC4Contract
class
HelloWorldContract
(
ARC4Contract
):
...
ARC-32 and ARC-56
¶
ARC32
extends the concepts in ARC4 to include an Application Specification which more holistically describes a smart contract and its associated state.
ARC-32/ARC-56 Application Specification files are automatically generated by the compiler for ARC4
contracts as
<ContractName>.arc32.json
or
<ContractName>.arc56.json
Methods
¶
Individual methods on a smart contract should be annotated with an
abimethod
decorator. This decorator is used to indicate a method which should be externally callable. The decorator itself includes properties to restrict when the method should be callable, for instance only when the application is being created or only when the OnComplete action is OptIn.
A method that should not be externally available should be annotated with a
subroutine
decorator.
Method docstrings will be used when outputting ARC-32 or ARC-56 application specifications, the following docstrings styles are supported ReST, Google, Numpydoc-style and Epydoc.
from
algopy
import
ARC4Contract
,
subroutine
,
arc4
class
HelloWorldContract
(
ARC4Contract
):
@arc4
.
abimethod
(
create
=
False
,
allow_actions
=
[
"NoOp"
,
"OptIn"
],
name
=
"external_name"
)
def
hello
(
self
,
name
:
arc4
.
String
)
->
arc4
.
String
:
return
self
.
internal_method
()
+
name
@subroutine
def
internal_method
(
self
)
->
arc4
.
String
:
return
arc4
.
String
(
"Hello, "
)
Router
¶
Algorand Smart Contracts only have two possible programs that are invoked when making an ApplicationCall Transaction (
appl
). The “clear state” program which is called when using an OnComplete action of
ClearState
or the “approval” program which is called for all other OnComplete actions.
Routing is required to dispatch calls handled by the approval program to the relevant ABI methods. When extending
ARC4Contract
, the routing code is automatically generated for you by the PuyaPy compiler.
Types
¶
ARC4 defines a number of
data types
which can be used in an ARC4 compatible contract and details how these types should be encoded in binary.
Algorand Python exposes these through a number of types which can be imported from the
algopy.arc4
module. These types represent binary encoded values following the rules prescribed in the ARC which can mean operations performed directly on these types are not as efficient as ones performed on natively supported types (such as
algopy.UInt64
or
algopy.Bytes
)
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.
Booleans
¶
Type:
algopy.arc4.Bool
Encoding:
A single byte where the most significant bit is
1
for
True
and
0
for
False
Native equivalent:
builtins.bool
Unsigned ints
¶
Types:
algopy.arc4.UIntN
(<= 64 bits)
algopy.arc4.BigUIntN
(> 64 bits)
Encoding:
A big endian byte array of N bits
Native equivalent:
algopy.UInt64
or
puya.py.BigUInt
Common bit sizes have also been aliased under
algopy.arc4.UInt8
,
algopy.arc4.UInt16
etc. A uint of any size between 8 and 512 bits (in intervals of 8bits) can be created using a generic parameter. It can be helpful to define your own alias for this type.
import
typing
as
t
from
algopy
import
arc4
UInt40
:
t
.
TypeAlias
=
arc4
.
UIntN
[
t
.
Literal
[
40
]]
Unsigned fixed point decimals
¶
Types:
algopy.arc4.UFixedNxM
(<= 64 bits)
algopy.arc4.BigUFixedNxM
(> 64 bits)
Encoding:
A big endian byte array of N bits where
encoded_value
=
value
/
(10^M)
Native equivalent:
none
import
typing
as
t
from
algopy
import
arc4
Decimal
:
t
.
TypeAlias
=
arc4
.
UFixedNxM
[
t
.
Literal
[
64
],
t
.
Literal
[
10
]]
Bytes and strings
¶
Types:
algopy.arc4.DynamicBytes
and
algopy.arc4.String
Encoding:
A variable length byte array prefixed with a 16-bit big endian header indicating the length of the data
Native equivalent:
algopy.Bytes
and
algopy.String
Strings are assumed to be utf-8 encoded and the length of a string is the total number of bytes,
not the total number of characters
.
Static arrays
¶
Type:
algopy.arc4.StaticArray
Encoding:
See
ARC4 Container Packing
Native equivalent:
none
An ARC4 StaticArray is an array of a fixed size. The item type is specified by the first generic parameter and the size is specified by the second.
import
typing
as
t
from
algopy
import
arc4
FourBytes
:
t
.
TypeAlias
=
arc4
.
StaticArray
[
arc4
.
Byte
,
t
.
Literal
[
4
]]
Address
¶
Type:
algopy.arc4.Address
Encoding:
A byte array 32 bytes long
Native equivalent:
algopy.Account
Address represents an Algorand address’s public key, and can be used instead of
algopy.Account
when needing to
reference an address in an ARC4 struct, tuple or return type. It is a subclass of
arc4.StaticArray[arc4.Byte,
typing.Literal[32]]
Dynamic arrays
¶
Type:
algopy.arc4.DynamicArray
Encoding:
See
ARC4 Container Packing
Native equivalent:
none
An ARC4 DynamicArray is an array of a variable size. The item type is specified by the first generic parameter. Items can be added and removed via
.pop
,
.append
, and
.extend
.
The current length of the array is encoded in a 16-bit prefix similar to the
arc4.DynamicBytes
and
arc4.String
types
import
typing
as
t
from
algopy
import
arc4
UInt64Array
:
t
.
TypeAlias
=
arc4
.
DynamicArray
[
arc4
.
UInt64
]
Tuples
¶
Type:
algopy.arc4.Tuple
Encoding:
See
ARC4 Container Packing
Native equivalent:
builtins.tuple
ARC4 Tuples are immutable statically sized arrays of mixed item types. Item types can be specified via generic parameters or inferred from constructor parameters.
Structs
¶
Type:
algopy.arc4.Struct
Encoding:
See
ARC4 Container Packing
Native equivalent:
typing.NamedTuple
ARC4 Structs are named tuples. The class keyword
frozen
can be used to indicate if a struct can be mutated.
Items can be accessed and mutated via names instead of indexes. Structs do not have a
.native
property,
but a NamedTuple can be used in ABI methods are will be encoded/decode to an ARC4 struct automatically.
import
typing
from
algopy
import
arc4
Decimal
:
typing
.
TypeAlias
=
arc4
.
UFixedNxM
[
typing
.
Literal
[
64
],
typing
.
Literal
[
9
]]
class
Vector
(
arc4
.
Struct
,
kw_only
=
True
,
frozen
=
True
):
x
:
Decimal
y
:
Decimal
ARC4 Container Packing
¶
ARC4 encoding rules are detailed explicitly in the
ARC
. A summary is included here.
Containers are composed of a head and tail portion.
For dynamic arrays, the head is prefixed with the length of the array encoded as a 16-bit number. This prefix is not included in offset calculation
For fixed sized items (eg. Bool, UIntN, or a StaticArray of UIntN), the item is included in the head
Consecutive Bool items are compressed into the minimum number of whole bytes possible by using a single bit to represent each Bool
For variable sized items (eg. DynamicArray, String etc), a pointer is included to the head and the data is added to the tail. This pointer represents the offset from the start of the head to the start of the item data in the tail.
Reference types
¶
Types:
algopy.Account
,
algopy.Application
,
algopy.Asset
,
algopy.gtxn.PaymentTransaction
,
algopy.gtxn.KeyRegistrationTransaction
,
algopy.gtxn.AssetConfigTransaction
,
algopy.gtxn.AssetTransferTransaction
,
algopy.gtxn.AssetFreezeTransaction
,
algopy.gtxn.ApplicationCallTransaction
The ARC4 specification allows for using a number of
reference types
in an ABI method signature where this reference type refers to…
another transaction in the group
an account in the accounts array (
apat
property of the transaction)
an asset in the foreign assets array (
apas
property of the transaction)
an application in the foreign apps array (
apfa
property of the transaction)
These types can only be used as parameters, and not as return types.
from
algopy
import
(
Account
,
Application
,
ARC4Contract
,
Asset
,
arc4
,
gtxn
,
)
class
Reference
(
ARC4Contract
):
@arc4
.
abimethod
def
with_transactions
(
self
,
asset
:
Asset
,
pay
:
gtxn
.
PaymentTransaction
,
account
:
Account
,
app
:
Application
,
axfr
:
gtxn
.
AssetTransferTransaction
)
->
None
:
...
Mutability
¶
To ensure semantic compatability the compiler will also check for any usages of mutable ARC4 types (arrays and structs) and ensure that any additional references are copied using the
.copy()
method.
Python values are passed by reference, and when an object (eg. an array or struct) is mutated in one place, all references to that object see the mutated version. In Python this is managed via the heap.
In Algorand Python these mutable values are instead stored on the stack, so when an additional reference is made (i.e. by assigning to another variable) a copy is added to the stack.
Which means if one reference is mutated, the other references would not see the change.
In order to keep the semantics the same, the compiler forces the addition of
.copy()
each time a new reference to the same object to match what will happen on the AVM.
Struct types can be indicated as
frozen
which will eliminate the need for a
.copy()
as long as the struct also contains no mutable fields (such as arrays or another mutable struct)
Next
ARC-28: Structured event logging
Previous
Opcode budgets
Copyright © 2024, Algorand Foundation
Made with
Sphinx
and
@pradyunsg
's
Furo


Answer:
Back to top
View this page
Toggle Light / Dark / Auto color theme
Toggle table of contents sidebar
ARC-28: Structured event logging
¶
ARC-28
provides a methodology for structured logging by Algorand smart contracts. It introduces the concept of Events, where data contained in logs may be categorized and structured.
Each Event is identified by a unique 4-byte identifier derived from its
Event
Signature
. The Event Signature is a UTF-8 string comprised of the event’s name, followed by the names of the
ARC-4
data types contained in the event, all enclosed in parentheses (
EventName(type1,type2,...)
) e.g.:
Swapped
(
uint64
,
uint64
)
Events are emitting by including them in the
log output
. The metadata that identifies the event should then be included in the ARC-4 contract output so that a calling client can parse the logs to parse the structured data out. This part of the ARC-28 spec isn’t yet implemented in Algorand Python, but it’s on the roadmap.
Emitting Events
¶
To emit an ARC-28 event in Algorand Python you can use the
emit
function, which appears in the
algopy.arc4
namespace for convenience since it heavily uses ARC-4 types and is essentially an extension of the ARC-4 specification. This function takes care of encoding the event payload to conform to the ARC-28 specification and there are 3 overloads:
An
ARC-4 struct
, from what the name of the struct will be used as a the event name and the struct parameters will be used as the event fields -
arc4.emit(Swapped(a,
b))
An event signature as a
string literal (or module variable)
, followed by the values -
arc4.emit("Swapped(uint64,uint64)",
a,
b)
An event name as a
string literal (or module variable)
, followed by the values -
arc4.emit("Swapped",
a,
b)
Here’s an example contract that emits events:
from
algopy
import
ARC4Contract
,
arc4
class
Swapped
(
arc4
.
Struct
):
a
:
arc4
.
UInt64
b
:
arc4
.
UInt64
class
EventEmitter
(
ARC4Contract
):
@arc4
.
abimethod
def
emit_swapped
(
self
,
a
:
arc4
.
UInt64
,
b
:
arc4
.
UInt64
)
->
None
:
arc4
.
emit
(
Swapped
(
b
,
a
))
arc4
.
emit
(
"Swapped(uint64,uint64)"
,
b
,
a
)
arc4
.
emit
(
"Swapped"
,
b
,
a
)
It’s worth noting that the ARC-28 event signature needs to be known at compile time so the event name can’t be a dynamic type and must be a static string literal or string module constant. If you want to emit dynamic events you can do so using the
log
method
, but you’d need to manually construct the correct series of bytes and the compiler won’t be able to emit the ARC-28 metadata so you’ll need to also manually parse the logs in your client.
Examples of manually constructing an event:
# This is essentially what the `emit` method is doing, noting that a,b need to be encoded
#   as a tuple so below (simple concat) only works for static ARC-4 types
log
(
arc4
.
arc4_signature
(
"Swapped(uint64,uint64)"
),
a
,
b
)
# or, if you wanted it to be truly dynamic for some reason,
#   (noting this has a non-trivial opcode cost) and assuming in this case
#   that `event_suffix` is already defined as a `String`:
event_name
=
String
(
"Event"
)
+
event_suffix
event_selector
=
op
.
sha512_256
((
event_name
+
"(uint64)"
)
.
bytes
)[:
4
]
log
(
event_selector
,
UInt64
(
6
))
Next
Calling other applications
Previous
ARC-4: Application Binary Interface
Copyright © 2024, Algorand Foundation
Made with
Sphinx
and
@pradyunsg
's
Furo


Answer:
Back to top
View this page
Toggle Light / Dark / Auto color theme
Toggle table of contents sidebar
Calling other applications
¶
The preferred way to call other smart contracts is using
algopy.arc4.abi_call
,
algopy.arc4.arc4_create
or
algopy.arc4.arc4_update
. These methods support type checking and encoding of arguments, decoding of results, group transactions,
and in the case of
arc4_create
and
arc4_update
automatic inclusion of approval and clear state programs.
algopy.arc4.abi_call
¶
algopy.arc4.abi_call
can be used to call other ARC4 contracts, the first argument should refer to
an ARC4 method either by referencing an Algorand Python
algopy.arc4.ARC4Contract
method,
an
algopy.arc4.ARC4Client
method generated from an ARC-32 app spec, or a string representing
the ARC4 method signature or name.
The following arguments should then be the arguments required for the call, these arguments will be type checked and converted where appropriate.
Any other related transaction parameters such as
app_id
,
fee
etc. can also be provided as keyword arguments.
If the ARC4 method returns an ARC4 result then the result will be a tuple of the ARC4 result and the inner transaction.
If the ARC4 method does not return a result, or if the result type is not fully qualified then just the inner transaction is returned.
from
algopy
import
Application
,
ARC4Contract
,
String
,
arc4
,
subroutine
class
HelloWorld
(
ARC4Contract
):
@arc4
.
abimethod
()
def
greet
(
self
,
name
:
String
)
->
String
:
return
"Hello "
+
name
@subroutine
def
call_existing_application
(
app
:
Application
)
->
None
:
greeting
,
greet_txn
=
arc4
.
abi_call
(
HelloWorld
.
greet
,
"there"
,
app_id
=
app
)
assert
greeting
==
"Hello there"
assert
greet_txn
.
app_id
==
1234
Alternative ways to use
arc4.abi_call
¶
ARC4Client method
¶
A ARC4Client client represents the ARC4 abimethods of a smart contract and can be used to call abimethods in a type safe way
ARC4Client’s can be produced by using
puyapy
--output-client=True
when compiling a smart contract
(this would be useful if you wanted to publish a client for consumption by other smart contracts)
An ARC4Client can also be be generated from an ARC-32 application.json using
puyapy-clientgen
e.g.
puyapy-clientgen
examples/hello_world_arc4/out/HelloWorldContract.arc32.json
, this would be
the recommended approach for calling another smart contract that is not written in Algorand Python or does not provide the source
from
algopy
import
arc4
,
subroutine
class
HelloWorldClient
(
arc4
.
ARC4Client
):
def
hello
(
self
,
name
:
arc4
.
String
)
->
arc4
.
String
:
...
@subroutine
def
call_another_contract
()
->
None
:
# can reference another algopy contract method
result
,
txn
=
arc4
.
abi_call
(
HelloWorldClient
.
hello
,
arc4
.
String
(
"World"
),
app
=...
)
assert
result
==
"Hello, World"
Method signature or name
¶
An ARC4 method selector can be used e.g.
"hello(string)string
along with a type index to specify the return type.
Additionally just a name can be provided and the method signature will be inferred e.g.
from
algopy
import
arc4
,
subroutine
@subroutine
def
call_another_contract
()
->
None
:
# can reference a method selector
result
,
txn
=
arc4
.
abi_call
[
arc4
.
String
](
"hello(string)string"
,
arc4
.
String
(
"Algo"
),
app
=...
)
assert
result
==
"Hello, Algo"
# can reference a method name, the method selector is inferred from arguments and return type
result
,
txn
=
arc4
.
abi_call
[
arc4
.
String
](
"hello"
,
"There"
,
app
=...
)
assert
result
==
"Hello, There"
algopy.arc4.arc4_create
¶
algopy.arc4.arc4_create
can be used to create ARC4 applications, and will automatically populate required fields for app creation (such as approval program, clear state program, and global/local state allocation).
Like
algopy.arc4.abi_call
it handles ARC4 arguments and provides ARC4 return values.
If the compiled programs and state allocation fields need to be customized (for example due to
template variables
),
this can be done by passing a
algopy.CompiledContract
via the
compiled
keyword argument.
from
algopy
import
ARC4Contract
,
String
,
arc4
,
subroutine
class
HelloWorld
(
ARC4Contract
):
@arc4
.
abimethod
()
def
greet
(
self
,
name
:
String
)
->
String
:
return
"Hello "
+
name
@subroutine
def
create_new_application
()
->
None
:
hello_world_app
=
arc4
.
arc4_create
(
HelloWorld
)
.
created_app
greeting
,
_txn
=
arc4
.
abi_call
(
HelloWorld
.
greet
,
"there"
,
app_id
=
hello_world_app
)
assert
greeting
==
"Hello there"
algopy.arc4.arc4_update
¶
algopy.arc4.arc4_update
is used to update an existing ARC4 application and will automatically populate the required approval and clear state program fields.
Like
algopy.arc4.abi_call
it handles ARC4 arguments and provides ARC4 return values.
If the compiled programs need to be customized (for example due to (for example due to
template variables
),
this can be done by passing a
algopy.CompiledContract
via the
compiled
keyword argument.
from
algopy
import
Application
,
ARC4Contract
,
String
,
arc4
,
subroutine
class
NewApp
(
ARC4Contract
):
@arc4
.
abimethod
()
def
greet
(
self
,
name
:
String
)
->
String
:
return
"Hello "
+
name
@subroutine
def
update_existing_application
(
existing_app
:
Application
)
->
None
:
hello_world_app
=
arc4
.
arc4_update
(
NewApp
,
app_id
=
existing_app
)
greeting
,
_txn
=
arc4
.
abi_call
(
NewApp
.
greet
,
"there"
,
app_id
=
hello_world_app
)
assert
greeting
==
"Hello there"
Using
itxn.ApplicationCall
¶
If the application being called is not an ARC4 contract, or an application specification is not available,
then
algopy.itxn.ApplicationCall
can be used. This approach is generally more verbose
than the above approaches, so should only be used if required. See
here
for an example
Next
Compiling to AVM bytecode
Previous
ARC-28: Structured event logging
Copyright © 2024, Algorand Foundation
Made with
Sphinx
and
@pradyunsg
's
Furo


Answer:
Back to top
View this page
Toggle Light / Dark / Auto color theme
Toggle table of contents sidebar
Compiling to AVM bytecode
¶
The PuyaPy compiler can compile Algorand Python smart contracts directly into AVM bytecode.
Once compiled, this bytecode can be utilized to construct AVM Application Call transactions both on and off chain.
Outputting AVM bytecode from CLI
¶
The
--output-bytecode
option can be used to generate
.bin
files for smart contracts and logic signatures, producing an approval and clear program for each smart contract.
Obtaining bytecode within other contracts
¶
The
compile_contract
function takes an Algorand Python smart contract class and returns a
CompiledContract
,
The global state, local state and program pages allocation parameters are derived from the contract by default, but can be overridden.
This compiled contract can then be used to create an
algopy.itxn.ApplicationCall
transaction or used with the
ARC4
functions.
The
compile_logicsig
takes an Algorand Python logic signature and returns a
CompiledLogicSig
, which can be used to
verify if a transaction has been signed by a particular logic signature.
Template variables
¶
Algorand Python supports defining
algopy.TemplateVar
variables that can be substituted during compilation.
For example, the following contract has
UInt64
and
Bytes
template variables.
templated_contract.py
¶
from
algopy
import
ARC4Contract
,
Bytes
,
TemplateVar
,
UInt64
,
arc4
class
TemplatedContract
(
ARC4Contract
):
@arc4
.
abimethod
def
my_method
(
self
)
->
UInt64
:
return
TemplateVar
[
UInt64
](
"SOME_UINT"
)
@arc4
.
abimethod
def
my_other_method
(
self
)
->
Bytes
:
return
TemplateVar
[
Bytes
](
"SOME_BYTES"
)
When compiling to bytecode, the values for these template variables must be provided. These values can be provided via the CLI,
or through the
template_vars
parameter of the
compile_contract
and
compile_logicsig
functions.
CLI
¶
The
--template-var
option can be used to
define
each variable.
For example to provide the values for the above example contract the following command could be used
puyapy
--template-var
SOME_UINT=123
--template-var
SOME_BYTES=0xABCD
templated_contract.py
Within other contracts
¶
The functions
compile_contract
and
compile_logicsig
both have an optional
template_vars
parameter
which can be used to define template variables. Variables defined in this manner take priority over variables defined on the CLI.
from
algopy
import
Bytes
,
UInt64
,
arc4
,
compile_contract
,
subroutine
from
templated_contract
import
TemplatedContract
@subroutine
def
create_templated_contract
()
->
None
:
compiled
=
compile_contract
(
TemplatedContract
,
global_uints
=
2
,
# customize allocated global uints
template_vars
=
{
# provide template vars
"SOME_UINT"
:
UInt64
(
123
),
"SOME_BYTES"
:
Bytes
(
b
"
\xAB\xCD
"
)
},
)
arc4
.
arc4_create
(
TemplatedContract
,
compiled
=
compiled
)
Next
Unsupported Python features
Previous
Calling other applications
Copyright © 2024, Algorand Foundation
Made with
Sphinx
and
@pradyunsg
's
Furo


Answer:
Back to top
View this page
Toggle Light / Dark / Auto color theme
Toggle table of contents sidebar
Unsupported Python features
¶
raise, try/except/finally
¶
Exception raising and exception handling constructs are not supported.
Supporting user exceptions would be costly to implement in terms of op codes.
Furthermore, AVM errors and exceptions are not “catch-able”, they immediately terminate the
program.
Therefore, there is very little to no benefit of supporting exceptions and exception handling.
The preferred method of raising an error that terminates is through the use of
assert statements
.
with
¶
Context managers are redundant without exception handling support.
async
¶
The AVM is not just single threaded, but all operations are effectively “blocking”, rendering
asynchronous programming effectively useless.
closures & lambdas
¶
Without the support of function pointers, or other methods of invoking an arbitrary function,
it’s not possible to return a function as a closure.
Nested functions/lambdas as a means of repeating common operations within a given function may be
supported in the future.
global keyword
¶
Module level values are only allowed to be
constants
. No
rebinding of module constants is allowed. It’s not clear what the meaning here would be, since
there’s no real arbitrary means of storing state without associating it with a particular contract.
If you do have need of such a thing, take a look at
gload_bytes
or
gload_uint64
if the contracts are within the same transaction,
otherwise
AppGlobal.get_ex_bytes
and
AppGlobal.get_ex_uint64
.
Inheritance (outside of contract classes)
¶
Polymorphism is also impossible to support without function pointers, so data classes (such as
arc4.Struct
) don’t currently allow for inheritance. Member functions there
are not supported because we’re not sure yet whether it’s better to not have inheritance but allow
functions on data classes, or to allow inheritance and disallow member functions.
Contract inheritance is a special case, since each concrete contract is compiled separately, true
polymorphism isn’t required as all references can be resolved at compile time.
Next
Principles & Background
Previous
Compiling to AVM bytecode
Copyright © 2024, Algorand Foundation
Made with
Sphinx
and
@pradyunsg
's
Furo


Answer:
Back to top
View this page
Toggle Light / Dark / Auto color theme
Toggle table of contents sidebar
Principles & Background
¶
Background
¶
Smart contracts
on the Algorand blockchain run on the Algorand Virtual Machine (
AVM
).
This is a stack based virtual machine, which executes AVM bytecode as part of an
Application Call transaction
.
The official mechanism for generating this bytecode is by submitting TEAL (Transaction Execution Approval Language) to
an Algorand Node to compile.
Smart signatures
have the same basis in the AVM and TEAL, but have a different execution model, one not involving
Application Call transactions. Our focus will primarily be on smart contracts, since they are strictly more powerful
in terms of available AVM functions.
TEAL is a
non-structured
imperative language
(albeit one with support for procedure calls that can isolate stack changes since v8 with
proto
). Writing TEAL is very
similar to writing assembly code. It goes without saying that this is a particularly common or well-practiced model for
programming these days.
As it stands today, developers wanting to write smart contracts specifically for Algorand have the option of writing
TEAL directly, or using some other mechanism of generating TEAL such as the officially supported
PyTEAL
or the community supported
tealish
.
PyTEAL follows a
generative programming
paradigm,
which is a form of metaprogramming. Naturally, writing programs to generate programs presents an additional hurdle for
developers looking to pick up smart contract development. Tooling support for this is also suboptimal, for example, many
classes of errors resulting from the interaction between the procedural elements of the Python language and the PyTEAL
expression-building framework go unnoticed until the point of TEAL generation, or worse go completely unnoticed, and even
when PyTEAL can/does provide an error it can be difficult to understand.
Tealish provides a higher level procedural language, bearing a passing resemblance to Python, than compiles down to TEAL.
However, it’s still lower level than most developers are used to.
For example, the expression
1
+
2
+
3
is
not valid in tealish
.
Another difference vs a higher level language such as Python is that
functions can only be declared after the program
entry point logic
.
In essence, tealish abstracts away many difficulties with writing plain TEAL,
but it is still essentially more of a transpiler than a compiler.
Furthermore, whilst appearing to have syntax inspired by Python, it both adds and removes many fundamental syntax elements,
presenting an additional learning curve to developers looking to learn blockchain development on Algorand.
Being a bespoke language also means it has a much smaller ecosystem of tooling built around it compared to languages like
Python or JavaScript.
To most developers, the Python programming language needs no introduction. First released in 1991, it’s popularity has
grown steadily over the decades, and as of June 2023 it is consistently ranked as either the most popular langauge,
or second most popular following JavaScript:
GitHub 2022
StackOverflow 2023
Tiobe
PYPL
The AlgoKit project is an Algorand Foundation initiative to improve the developer experience on Algorand. Within this
broad remit, two of the key
principles
are to “meet developers where they are” and “leverage existing ecosystem”.
Building a compiler that allows developers to write smart contracts using an idiomatic subset of a high level language
such as Python would make great strides towards both of these goals.
Wyvern was the original internal code name for just such a compiler (now called Puya), one that will transform Python code into valid TEAL
smart contracts. In line with the principle of meeting developers where they are, and recognising the popularity of
JavaScript and TypeScript, a parallel initiative to build a TypeScript to TEAL compiler is
also underway
.
Principles
¶
The principles listed here should form the basis of our decision-making, both in the design and implementation.
Least surprise
¶
Our primary objective is to assist developers in creating accurate smart contracts right from the
start. The often immutable nature of these contracts - although not always the case - and the
substantial financial value they frequently safeguard, underlines the importance of this goal.
This principle ensures that the code behaves as anticipated by the developer. Specifically, if
you’re a Python developer writing Python smart contract code, you can expect the code to behave
identically to its execution in a standard Python environment.
Furthermore, we believe in promoting explicitness and correctness in contract code and its
associated typing. This approach reduces potential errors and enhances the overall integrity of our
smart contracts. Our commitment is to provide a user-friendly platform that aligns with the
developer’s intuition and experience, ultimately simplifying their work and minimizing the
potential for mistakes.
Inherited from AlgoKit
¶
As a part of the AlgoKit project, the principles outlined
there
also apply - to the extent that this project is just one component of AlgoKit.
“Leverage existing ecosystem”
¶
AlgoKit functionality gets into the hands of Algorand developers quickly by building on top of the
existing ecosystem wherever possible and aligned to these principles.
In order to leverage as much existing Python tooling as possible, we should strive to maintain the highest level of
compatibility with the Python language (and the reference implementation: CPython).
“Meet developers where they are”
¶
Make Blockchain development mainstream by giving all developers an idiomatic development experience in the operating
system, IDE and language they are comfortable with so they can dive in quickly and have less they need to learn before
being productive.
Python is a very idiomatic language. We should embrace accepted patterns and practices as much as possible,
such as those listed in
PEP-20
(aka “The Zen of Python”).
“Extensible”
¶
Be extensible for community contribution rather than stifling innovation, bottle-necking all changes through the
Algorand Foundation and preventing the opportunity for other ecosystems being represented (e.g. Go, Rust, etc.).
This helps make developers feel welcome and is part of the developer experience, plus it makes it easier to add
features sustainably
One way to support this principle in the broader AlgoKit context is by building in a mechanism for reusing
common code between smart contracts, to allow the community to build their own Python packages.
“Sustainable”
¶
AlgoKit should be built in a flexible fashion with long-term maintenance in mind. Updates to latest patches in
dependencies, Algorand protocol development updates, and community contributions and feedback will all feed in to the
evolution of the software.
Taking this principle further, ensuring the compiler is well-designed (e.g. frontend backend separation,
with a well-thought-out IR) will help with maintaining and improving the implementation over time. For example,
adding in new TEAL language features will be easier, same for implementing new optimisation strategies.
Looking to the future, best practices for smart contract development are rapidly evolving. We shouldn’t tie the
implementation too tightly to a current standard such as ARC-4 - although in that specific example, we would still
aim for first class support, but it shouldn’t be assumed as the only way to write smart contracts.
“Modular components”
¶
Solution components should be modular and loosely coupled to facilitate efficient parallel development by small,
effective teams, reduced architectural complexity and allowing developers to pick and choose the specific tools and
capabilities they want to use based on their needs and what they are comfortable with.
We will focus on the language and compiler design itself.
An example of a very useful feature, that is strongly related but could be implemented separately instead,
is the ability to run the users code in a unit-testing context, without compilation+deployment first.
This would require implementing in Python some level of simulation of Algorand Nodes / AVM behaviour.
“Secure by default”
¶
Include defaults, patterns and tooling that help developers write secure code and reduce the likelihood of security
incidents in the Algorand ecosystem. This solution should help Algorand be the most secure Blockchain ecosystem.
Enforcing security (which is multi-faceted) at a compiler level is difficult, and is some cases impossible.
The best application of this principle here is to support auditing, which is important and nuanced enough to be
listed below as a separate principle.
“Cohesive developer tool suite” + “Seamless onramp”
¶
Cohesive developer tool suite: Using AlgoKit should feel professional and cohesive, like it was designed to work
together, for the developer; not against them. Developers are guided towards delivering end-to-end, high quality
outcomes on MainNet so they and Algorand are more likely to be successful.
Seamless onramp: New developers have a seamless experience to get started and they are guided into a pit of success
with best practices, supported by great training collateral; you should be able to go from nothing to debugging code
in 5 minutes.
These principles relate more to AlgoKit as a whole, so we can respect them by considering the impacts of our decisions
there more broadly.
Abstraction without obfuscation
¶
Algorand Python is a high level language, with support for things such as branching logic, operator precedence, etc.,
and not a set of “macros” for generating TEAL. As such, developers will not be able to directly influence specific TEAL
output, if this is desirable a language such as
Tealish
is more appropriate.
Whilst this will abstract away certain aspects of the underlying TEAL language, there are certain AVM concerns
(such as op code budgets) that should not be abstracted away. That said, we should strive to generate code this is
cost-effective and unsurprising. Python mechanisms such as dynamic (runtime) dispatch, and also many of its builtin
functions on types such as
str
that are taken for granted, would require large amounts of ops compared to the
Python code it represents.
Support auditing
¶
Auditing is a critical part of the security process for deploying smart contracts. We want to support this function,
and can do so in two ways:
By ensuring the same Python code as input generates identical output each time the compiler
is run regardless of the system it’s running on. This is what might be termed
Output stability
.
Ensuring a consistent output regardless of the system it’s run on (assuming the same compiler version), means that
auditing the lower level (ie TEAL) code is possible.
Although auditing the TEAL code should be possible, being able to easily identify and relate it back to the higher level
code can make auditing the contract logic simpler and easier.
Revolution, not evolution
¶
This is a new and groundbreaking way of developing for Algorand, and not a continuation of the PyTEAL/Beaker approach.
By allowing developers to write procedural code, as opposed to constructing an expression tree,
we can (among other things) significantly reduce the barrier to entry for developing smart contracts for the Algorand platform.
Since the programming paradigm will be fundamentally different, providing a smooth migration experience from PyTEAL
to this new world is not an intended goal, and shouldn’t be a factor in our decisions. For example, it is not a goal of
this project to produce a step-by-step “migrating from PyTEAL” document, as it is not a requirement for users to
switch to this new paradigm in the short to medium term - support for PyTEAL should continue in parallel.
Next
API Reference
Previous
Unsupported Python features
Copyright © 2024, Algorand Foundation
Made with
Sphinx
and
@pradyunsg
's
Furo


Answer:
Back to top
View this page
Toggle Light / Dark / Auto color theme
Toggle table of contents sidebar
API Reference
¶
algopy
Package Contents
Classes
Functions
Data
API
ARC4Contract
Account
Application
Asset
BigUInt
Box
BoxMap
BoxRef
Bytes
BytesBacked
CompiledContract
CompiledLogicSig
Contract
Global
GlobalState
LocalState
LogicSig
OnCompleteAction
OpUpFeeSource
StateTotals
String
TemplateVar
TransactionType
Txn
UInt64
compile_contract()
compile_logicsig()
ensure_budget()
log()
logicsig()
subroutine()
uenumerate
urange
algopy.arc4
Module Contents
Classes
Functions
Data
API
ARC4Client
ARC4Contract
Address
BigUFixedNxM
BigUIntN
Bool
Byte
DynamicArray
DynamicBytes
StaticArray
String
Struct
Tuple
UFixedNxM
UInt128
UInt16
UInt256
UInt32
UInt512
UInt64
UInt8
UIntN
abi_call
abimethod()
arc4_create()
arc4_signature()
arc4_update()
baremethod()
emit()
algopy.gtxn
Module Contents
Classes
API
ApplicationCallTransaction
AssetConfigTransaction
AssetFreezeTransaction
AssetTransferTransaction
KeyRegistrationTransaction
PaymentTransaction
Transaction
TransactionBase
algopy.itxn
Module Contents
Classes
Functions
API
ApplicationCall
ApplicationCallInnerTransaction
AssetConfig
AssetConfigInnerTransaction
AssetFreeze
AssetFreezeInnerTransaction
AssetTransfer
AssetTransferInnerTransaction
InnerTransaction
InnerTransactionResult
KeyRegistration
KeyRegistrationInnerTransaction
Payment
PaymentInnerTransaction
submit_txns()
algopy.op
Module Contents
Classes
Functions
API
AcctParamsGet
AppGlobal
AppLocal
AppParamsGet
AssetHoldingGet
AssetParamsGet
Base64
Block
Box
EC
ECDSA
EllipticCurve
GITxn
GTxn
Global
ITxn
ITxnCreate
JsonRef
MiMCConfigurations
Scratch
Txn
VoterParamsGet
VrfVerify
addw()
app_opted_in()
arg()
balance()
base64_decode()
bitlen()
bsqrt()
btoi()
bzero()
concat()
divmodw()
divw()
ecdsa_pk_decompress()
ecdsa_pk_recover()
ecdsa_verify()
ed25519verify()
ed25519verify_bare()
err()
exit()
exp()
expw()
extract()
extract_uint16()
extract_uint32()
extract_uint64()
falcon_verify()
gaid()
getbit()
getbyte()
gload_bytes()
gload_uint64()
itob()
keccak256()
mimc()
min_balance()
mulw()
online_stake()
replace()
select_bytes()
select_uint64()
setbit_bytes()
setbit_uint64()
setbyte()
sha256()
sha3_256()
sha512_256()
shl()
shr()
sqrt()
substring()
sumhash512()
vrf_verify()
Next
algopy
Previous
Principles & Background
Copyright © 2024, Algorand Foundation
Made with
Sphinx
and
@pradyunsg
's
Furo