Control Flow
PyTeal provides several control flow expressions to create programs.
Exiting the Program: Approve and Reject
Note
The Approve and Reject expressions are only available in program version 4 or higher.
Prior to this, Return(Int(1)) is equivalent to Approve() and Return(Int(0))
is equivalent to Reject().
The Approve and Reject expressions cause the program to immediately exit. If Approve
is used, then the execution is marked as successful, and if Reject is used, then the execution
is marked as unsuccessful.
These expressions also work inside subroutines. When used inside subroutines, they
also cause the program to immediately exit, contrary to Return(...) which just returns from the subroutine.
Chaining Expressions: Seq
The Seq expression can be used to create a sequence of multiple expressions. It’s arguments are the
expressions to include in the sequence, either as a variable number of arguments, or as a single list
For example:
Seq(
App.globalPut(Bytes("creator"), Txn.sender()),
Return(Int(1))
)
A Seq expression will take on the value of its last expression. Additionally, all
expressions in a Seq expression, except the last one, must not return anything (e.g.
evaluate to TealType.none). This restriction is in place because intermediate values must not
add things to the TEAL stack. As a result, the following is an invalid sequence:
Seq(
Txn.sender(),
Return(Int(1))
)
If you must include an operation that returns a value in the earlier
part of a sequence, you can wrap the value in a Pop expression to discard it. For example,
Seq(
Pop(Txn.sender()),
Return(Int(1))
)
Simple Branching: If
In an If expression,
If(test-expr, then-expr, else-expr)
the test-expr is always evaluated and needs to be typed TealType.uint64.
If it results in a value greater than 0, then the then-expr is evaluated.
Otherwise, else-expr is evaluated. Note that then-expr and else-expr must
evaluate to the same type (e.g. both TealType.uint64).
You may also invoke an If expression without an else-expr:
If(test-expr, then-expr)
In this case, then-expr must be typed TealType.none.
There is also an alternate way to write an If expression that makes reading
complex statements easier to read.
If(test-expr)
.Then(then-expr)
.ElseIf(test-expr)
.Then(then-expr)
.Else(else-expr)
Checking Conditions: Assert
The Assert expression can be used to ensure that conditions are met before continuing the
program. The syntax for Assert is:
Assert(test-expr)
If test-expr is always evaluated and must be typed TealType.uint64. If
test-expr results in a value greater than 0, the program continues. Otherwise, the program
immediately exits and indicates that it encountered an error.
Example:
Assert(Txn.type_enum() == TxnType.Payment)
The above example will cause the program to immediately fail with an error if the transaction type is not a payment.
Chaining Tests: Cond
A Cond expression chains a series of tests to select a result expression.
The syntax of Cond is:
Cond([test-expr-1, body-1],
[test-expr-2, body-2],
. . . )
Each test-expr is evaluated in order. If it produces 0, the paired body
is ignored, and evaluation proceeds to the next test-expr.
As soon as a test-expr produces a true value (> 0),
its body is evaluated to produce the value for this Cond expression.
If none of test-expr s evaluates to a true value, the Cond expression will
be evaluated to err, a TEAL opcode that causes the runtime panic.
In a Cond expression, each test-expr needs to be typed TealType.uint64.
A body could be typed either TealType.uint64 or TealType.bytes. However, all
body s must have the same data type. Otherwise, a TealTypeError is triggered.
Example:
Cond([Global.group_size() == Int(5), bid],
[Global.group_size() == Int(4), redeem],
[Global.group_size() == Int(1), wrapup])
This PyTeal code branches on the size of the atomic transaction group.
Looping: While
Note
This expression is only available in program version 4 or higher.
The While expression can be used to create simple loops in PyTeal. The syntax of While is:
While(loop-condition).Do(loop-body)
The loop-condition expression must evaluate to TealType.uint64, and the loop-body
expression must evaluate to TealType.none.
The loop-body expression will continue to execute as long as loop-condition produces
a true value (> 0).
For example, the following code uses ScratchVar to iterate through every transaction in the
current group and sum up all of their fees.
totalFees = ScratchVar(TealType.uint64)
i = ScratchVar(TealType.uint64)
Seq([
i.store(Int(0)),
totalFees.store(Int(0)),
While(i.load() < Global.group_size()).Do(
totalFees.store(totalFees.load() + Gtxn[i.load()].fee()),
i.store(i.load() + Int(1))
)
])
Looping: For
Note
This expression is only available in program version 4 or higher.
Similar to While, the For expression can also be used to create loops in PyTeal. The
syntax of For is:
For(loop-start, loop-condition, loop-step).Do(loop-body)
The loop-start, loop-step, and loop-body expressions must evaluate to
TealType.none, and the the loop-condition expression must evaluate to TealType.uint64.
When a For expression is executed, loop-start is executed first. Then the
expressions loop-condition, loop-body, and loop-step will continue to
execute in order as long as loop-condition produces a true value (> 0).
For example, the following code uses ScratchVar to iterate through every transaction in the
current group and sum up all of their fees. The code here is functionally equivalent to the
While loop example above.
totalFees = ScratchVar(TealType.uint64)
i = ScratchVar(TealType.uint64)
Seq([
totalFees.store(Int(0)),
For(i.store(Int(0)), i.load() < Global.group_size(), i.store(i.load() + Int(1))).Do(
totalFees.store(totalFees.load() + Gtxn[i.load()].fee())
)
])
Exiting Loops: Continue and Break
The expressions Continue and Break can be used to exit While and For
loops in different ways.
When Continue is present in the loop body, it instructs the program to skip the remainder
of the loop body. The loop may continue to execute as long as its condition remains true.
For example, the code below iterates though every transaction in the current group and counts how
many are payments, using the Continue expression.
numPayments = ScratchVar(TealType.uint64)
i = ScratchVar(TealType.uint64)
Seq([
numPayments.store(Int(0)),
For(i.store(Int(0)), i.load() < Global.group_size(), i.store(i.load() + Int(1))).Do(
If(Gtxn[i.load()].type_enum() != TxnType.Payment)
.Then(Continue()),
numPayments.store(numPayments.load() + Int(1))
)
])
When Break is present in the loop body, it instructs the program to completely exit the
current loop. The loop will not continue to execute, even if its condition remains true.
For example, the code below finds the index of the first payment transaction in the current group,
using the Break expression.
firstPaymentIndex = ScratchVar(TealType.uint64)
i = ScratchVar(TealType.uint64)
Seq([
# store a default value in case no payment transactions are found
firstPaymentIndex.store(Global.group_size()),
For(i.store(Int(0)), i.load() < Global.group_size(), i.store(i.load() + Int(1))).Do(
If(Gtxn[i.load()].type_enum() == TxnType.Payment)
.Then(
firstPaymentIndex.store(i.load()),
Break()
)
),
# assert that a payment was found
Assert(firstPaymentIndex.load() < Global.group_size())
])
Subroutines
Note
Subroutines are only available in program version 4 or higher.
A subroutine is section of code that can be called multiple times from within a program. Subroutines are PyTeal’s equivalent to functions. Subroutine constraints include:
Subroutines accept any number of arguments.
Subroutine argument types can be any Expr (PyTeal expression) or strictly ScratchVar (no subclasses allowed). PyTeal applies pass-by-value semantics to Expr and pass-by-reference to ScratchVar.
Subroutines return a single value, or no value.
Creating Subroutines
To create a subroutine, apply the Subroutine function decorator to a Python function which
implements the subroutine. This decorator takes one argument, which is the return type of the subroutine.
TealType.none indicates that the subroutine does not return a value, and any other type
(e.g. TealType.uint64 or TealType.bytes) indicates the return type of the single value
the subroutine returns.
For example,
@Subroutine(TealType.uint64)
def isEven(i):
return i % Int(2) == Int(0)
PyTeal applies these parameter type annotation constraints when compiling subroutine definitions:
ScratchVarparameters require a type annotation.Exprparameters do not require a type annotation. PyTeal implicitly declares unannotated parameter types asExpr.
Here’s an example illustrating ScratchVar parameter declaration with parameter type annotations:
@Subroutine(TealType.none)
def swap(x: ScratchVar, y: ScratchVar):
z = ScratchVar(TealType.anytype)
return Seq(
z.store(x.load()),
x.store(y.load()),
y.store(z.load()),
)
Calling Subroutines
To call a subroutine, simply call it like a normal Python function and pass in its arguments. For example,
App.globalPut(Bytes("value_is_even"), isEven(Int(10)))
Recursion
Recursion with subroutines is also possible. For example, the subroutine below also checks if its argument is even, but uses recursion to do so.
@Subroutine(TealType.uint64)
def recursiveIsEven(i):
return (
If(i == Int(0))
.Then(Int(1))
.ElseIf(i == Int(1))
.Then(Int(0))
.Else(recursiveIsEven(i - Int(2)))
)
Recursion and ScratchVar’s
Recursion with parameters of type ScratchVar is disallowed. For example, the following subroutine is considered illegal and attempting compilation will result in a TealInputError:
@Subroutine(TealType.none)
def ILLEGAL_recursion(i: ScratchVar):
return (
If(i.load() == Int(0))
.Then(i.store(Int(1)))
.ElseIf(i.load() == Int(1))
.Then(i.store(Int(0)))
.Else(i.store(i.load() - Int(2)), ILLEGAL_recursion(i))
)
Exiting Subroutines
The Return expression can be used to explicitly return from a subroutine.
If the subroutine does not return a value, Return should be called with no arguments. For
example, the subroutine below asserts that the first payment transaction in the current group has a
fee of 0:
@Subroutine(TealType.none)
def assertFirstPaymentHasZeroFee():
i = ScratchVar(TealType.uint64)
return Seq([
For(i.store(Int(0)), i.load() < Global.group_size(), i.store(i.load() + Int(1))).Do(
If(Gtxn[i.load()].type_enum() == TxnType.Payment)
.Then(
Assert(Gtxn[i.load()].fee() == Int(0)),
Return()
)
),
# no payments found
Err()
])
Otherwise if the subroutine does return a value, that value should be the argument to the Return
expression. For example, the subroutine below checks whether the current group contains a payment
transaction:
@Subroutine(TealType.uint64)
def hasPayment():
i = ScratchVar(TealType.uint64)
return Seq([
For(i.store(Int(0)), i.load() < Global.group_size(), i.store(i.load() + Int(1))).Do(
If(Gtxn[i.load()].type_enum() == TxnType.Payment)
.Then(Return(Int(1)))
),
Return(Int(0))
])
Return can also be called from the main program. In this case, a single integer argument
should be provided, which is the success value for the current execution. A true value (> 0)
is equivalent to Approve, and a false value is equivalent to Reject.