Build on Algorand
Basic Smart Contract with Python
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 reuse 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:
...
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 (i.e. non-abstract) classes produce output artifacts for deployment. To mark a class as explicitly abstract, inherit from abc.ABC.
The compiler will produce a warning if a Contract class is implicitly abstract, i.e. if any abstract methods are unimplemented.
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
- scratch_slots
- state_totals
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-ARC-4 contract, the contract class must implement an approval_program and a clear_state_program method.
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 ARC-4 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 ARC-4 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. 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
- algopy.arc4.baremethod
- algopy.subroutine
Subroutines won’t be directly callable through the default router.
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.
This was just an introduction to developing Algorand smart contracts with Python. For more detailed information, you can use this link.
Example Smart Contracts
Below are example Algorand Python smart contracts that demonstrate the program structure and key features described above. Each example is followed by a brief explanation.
HelloWorld Contract
# pyright: reportMissingModuleSource=false
from algopy import ARC4Contract, String
from algopy.arc4 import abimethod
class HelloWorld(ARC4Contract):
@abimethod()
def hello(self, name: String) -> String:
return "Hello, " + name
- Inherits from ARC4Contract, making it ARC-4 compliant.
- Defines a single ABI method hello that takes a String and returns a greeting.
- Demonstrates how to expose a function as a smart contract method using the @abimethod decorator.
- Returns a personalized greeting when called.
CustomCreate Contract
# pyright: reportMissingModuleSource=false
from algopy import ARC4Contract, GlobalState, UInt64
from algopy.arc4 import abimethod
class CustomCreate(ARC4Contract):
def __init__(self) -> None:
self.age = GlobalState(UInt64)
@abimethod(create="require")
def custom_create(self, age: UInt64) -> None:
self.age.value = age
@abimethod()
def get_age(self) -> UInt64:
return self.age.value
- Inherits from ARC4Contract.
- Uses __init__ to define a global state variable age of type UInt64.
- The custom_create method (with create="require") must be called during contract creation and sets the initial value of age.
- The get_age method allows reading the current value of age from the contract’s global state.
- Demonstrates managing global state and custom initialization logic in an ARC-4 contract.
Comments
You need to enroll in the course to be able to comment!