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 TEAL 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.

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:

Invalid Seq expression
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 TEAL 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(Seq([
        totalFees.store(totalFees.load() + Gtxn[i.load()].fee()),
        i.store(i.load() + Int(1))
    ]))
])

Looping: For

Note

This expression is only available in TEAL 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(Seq([
        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(Seq([
            firstPaymentIndex.store(i.load()),
            Break()
        ]))
    ),
    # assert that a payment was found
    Assert(firstPaymentIndex.load() < Global.group_size())
])

Subroutines

Note

Subroutines are only available in TEAL 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:

  • ScratchVar parameters require a type annotation.

  • Expr parameters do not require a type annotation. PyTeal implicitly declares unannotated parameter types as Expr.

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(Seq(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(Seq([
                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.