Skip to content
On this page

Casting

The SmartPy compiler must know the type of all variables in order to compile to Michelson, which is a strongly-typed language. SmartPy has powerful type inference, which can determine the types of most variables based on how they are used.

However, sometimes it is necessary to cast a variable with sp.cast to specify its type. For example, the errors Error in generated Michelson contract: Missing type in storage or Missing type in parameters often mean that SmartPy can't determine the type of a variable in the contract storage or in an entrypoint parameter.

Other errors might lead you to think that you need to cast a variable when you don't. Errors that start with entrypoint expects parameter of type [type a], but got [type b] usually mean that you are passing the wrong type of variable to an entrypoint.

INFO

In SmartPy, casting does not change the type of a variable, like casting does in some other languages. Instead, casting identifies the type of a variable that the compiler can't infer from context.

The STDLIB modules provide some traditional casting functions, such as converting between different numerical types.

sp.cast(x, T) → T

Assign value x to type T.

Casting can set the types of entrypoint parameters and resolve type ambiguities:

smartpy
@sp.entrypoint
def ep(x, y):
    sp.cast(x, sp.int)
    sp.cast(y, sp.nat)
    ...

You can cast values either as a statement or as an expression, as in these examples:

smartpy
# Cast as an expression
b = sp.cast(a, sp.int) + 1

# Cast as a statement
c = 5
sp.cast(c, sp.nat)

When is casting not necessary?

Casting is not necessary when SmartPy can infer a variable type from the context. For example, casting is not necessary if you assign a value to a variable, because SmartPy can use the type of that value as the type of the variable, as in this example:

smartpy
# No casting required
my_string = "hello" # Inferred to be sp.string
my_list = [my_string] # Inferred to be sp.list[string]

Casting is also not necessary when SmartPy can infer a type based on an operation. For example, this entrypoint accepts a parameter that is not casted, but SmartPy can infer that it is a nat because the entrypoint adds it to another nat:

smartpy
@sp.entrypoint
def acceptANat(self, a):
    b = sp.nat(5)
    c = a + b  # All variables assumed to be sp.nat

SmartPy can infer types in this way across entrypoints. For example, the following contract's __init__() method creates an empty big map and its update entrypoint adds an element to that big map. SmartPy can infer the type of the big map by the element that the entrypoint adds:

smartpy
class MyContract(sp.Contract):
    def __init__(self):
        # Inferred to be sp.big_map[sp.string, sp.int] based on usage
        self.data.my_big_map = sp.big_map()

    @sp.entrypoint
    def update(self):
        self.data.my_big_map["a"] = 5

INFO

SmartPy does not use information from the test scenario to infer types. All type information comes from the contract code itself.

When does SmartPy assume types?

The SmartPy compiler sometimes assumes a variable's type when it does not have complete information.

When SmartPy knows that a variable is a number but not whether that number is an integer or a nat, it assumes that it is an integer. For example, this __init__() function creates a storage field and puts a number in it. In the absence of other information, SmartPy assumes that it is an integer:

smartpy
def __init__(self):
    self.data.my_value = 5  # Assumed to be sp.int

Similarly, SmartPy assumes that key-value series in braces are maps, not big maps, as in this example:

smartpy
def __init__(self):
    # Assumed to be sp.map[sp.string, sp.int]
    self.data.my_map = {'a': 1, 'b': 2}

When is casting necessary?

Because SmartPy can use many different ways of inferring types, casting is rarely necessary. Here are some cases that may require casting:

Setting the type of unused variables

Casting may be required during the development process when you create a variable but do not use it yet.

For example, this contract's __init__() method creates an empty big map. If no entrypoints manipulate this big map directly, you may need to cast it temporarily to specify its type:

smartpy
class MyContract(sp.Contract):
    def __init__(self):
        self.data.my_big_map = sp.big_map()
        sp.cast(self.data.my_big_map, sp.big_map[sp.string, sp.nat])

Of course, completed contracts don't usually have unused variables in them. Therefore, casting is usually necessary only during the development process, before your code is complete.

Setting the layout for parameters

Casting also allows you to set the layout of records in storage or parameters. For example, the FA2 standard specifies a layout and annotations for entrypoint parameters. The transfer entrypoint parameter receives this Micheline value:

michelson
(list %transfer
  (pair
    (address %from_)
    (list %txs
      (pair
        (address %to_)
        (pair
          (nat %token_id)
          (nat %amount)
        )
      )
    )
  )
)

To implement this layout in SmartPy and include the annotations, you can cast the fields in the parameter. The SmartPy FA2 library provides types for this purpose. In this case, the t.transfer_batch type sets the fields and annotations for the transfer entrypoint. These are the types:

smartpy
tx: type = sp.record(
    to_=sp.address,
    token_id=sp.nat,
    amount=sp.nat,
).layout(("to_", ("token_id", "amount")))

transfer_batch: type = sp.record(
    from_=sp.address,
    txs=list[tx],
).layout(("from_", "txs"))

transfer_params: type = list[transfer_batch]

Then, the transfer entrypoint casts its parameter to the t.transfer_batch type:

smartpy
@sp.entrypoint
def transfer(self, batch):
    """Accept a list of transfer operations between a source and multiple
    destinations.

    `transfer_tx_` and `is_defined_` must be defined in the child class.
    """
    sp.cast(batch, t.transfer_params)
    # ...

You may need to set layouts in this way to accept calls from contracts that provide parameters in a specific layout.

When is casting helpful?

Casting can be helpful (but is not required) to set the type of storage or of a parameter. If you set a type in this way by casting, the SmartPy compiler warns you if the code deviates from this type. For example, this code creates a type for the storage with comments that describe what each field is for. Then the __init__() method casts the storage to this type to ensure that the contract uses this type:

smartpy
contract_storage_type: type = sp.record(
    my_integer = sp.int,  # An integer value
    my_string = sp.string,  # A string value
    my_record = sp.record(
        my_address = sp.address,  # An address
        my_nat = sp.nat,  # A nat
        my_bool = sp.bool, # A Boolean
        my_list_nat = sp.list[sp.nat]  # A list of nat
    )
)

class MyContract(sp.Contract):
    def __init__(self):
        # Cast storage to alert on differences
        sp.cast(self.data, contract_storage_type)

Type aliases

To help with casting, you can create type aliases to identify types. These types can be simple or complex.

: type =  t → 

Alias a type.

smartpy
@sp.module
def main():
    # my_integer_type is now equivalent to sp.int
    my_integer_type: type = sp.int

    # A more complex type for the contract storage
    contract_storage_type: type = sp.record(
        ledger = sp.big_map[sp.pair[sp.address, sp.nat], sp.nat],
        supply = sp.big_map[sp.nat, sp.nat],
        metadata = sp.big_map[sp.string, sp.bytes],
        token_metadata = sp.big_map[
            sp.nat,
            sp.record(token_id=sp.nat, token_info=sp.map[sp.string, sp.bytes]).layout(
                ("token_id", "token_info")
            ),
        ]
    )

    class MyContract(sp.Contract):

        def __init__(self):
            self.data.ledger = sp.big_map()
            self.data.supply = sp.big_map()
            self.data.metadata = sp.big_map()
            self.data.token_metadata = sp.big_map()

            # Cast the storage to a type alias
            sp.cast(self.data, contract_storage_type)

        @sp.entrypoint
        def ep(x):
            # my_integer_type can be used anywhere you would use a type
            sp.cast(x, my_integer_type)