Skip to content

Local development

You can develop SmartPy contracts using our web IDE or your preferred IDE with a local installation. These tips can help you get the most out of your local development experience.

Adding virtual environments to path analysis

Including your Python virtual environment in your IDE's path settings helps tools like Pylance recognize SmartPy.

Example for VSCode / Cursor settings.json:

json
"python.analysis.extraPaths": [
    ".env/lib/python3.12/site-packages"
],

.spy syntax highlighting

Highlighting .spy files as .py files in your IDE can improve code readability.

Examples

You can find code examples here: https://gitlab.com/smartpy.io/smartpy/-/tree/main/wheels/tezos-smartpy-examples/smartpy_examples.

You may also find more examples in our tests, the FA2 library code, and FA2 library tests.

If you use an LLM, consider downloading these .py files and placing them somewhere the LLM can index. This can improve the quality of suggestions.

LLM instructions

If you work with an LLM assistant, you might want to share these instructions from llm.txt.

Details
---
description:
globs:
alwaysApply: true
---
# SmartPy

> SmartPy is a comprehensive solution for developing, testing, and deploying smart contracts on the Tezos blockchain. It provides a high-level, Python-like language with static type checking that transpiles to Michelson (Tezos' native smart contract language).

SmartPy code looks and behaves much like Python but is not actually Python. It is a domain-specific language with types, structures, and limitations specific to the Tezos blockchain. This documentation covers the latest SmartPy syntax (v0.17+), which uses modern Python features like type annotations and pattern matching. The older syntax using "T" prefixed types (like `sp.TInt`) is deprecated.

## File Pattern Matching

```
*.py
*.spy
```

This rule applies specifically to SmartPy (`*.py` and `*.spy`) contract and module files.

SmartPy files (`.spy`) contain only SmartPy code and can import other `.spy` files. A single `.py` file can contain both SmartPy code (in modules designated with the `@sp.module` decorator) and Python code (for test scenarios).

## Core Concepts

- [Modules](https://smartpy.tezos.com/manual/syntax/modules.html): SmartPy code is structured in modules defined using the `@sp.module` decorator or within `spy` files.
- [Type System](https://smartpy.tezos.com/manual/data-types/types.html): SmartPy has a rich type system including primitives (nat, int, string), collections (map, big_map, list), and user-defined types (record, variant).
- [Smart Contracts](https://smartpy.tezos.com/manual/syntax/contracts.html): Contracts are created by defining a class that inherits from `sp.Contract` or from another contract class. The public methods are called entrypoints.
- [Testing](https://smartpy.tezos.com/manual/scenarios/test_scenarios.html): SmartPy includes a scenario-based testing framework for contract verification.
- [Modules & Imports](https://smartpy.tezos.com/manual/syntax/modules.html): SmartPy supports modular development with imports from separate files.

## Syntax Reference

- [Type Definitions](https://smartpy.tezos.com/manual/data-types/types.html): Define types using the `: type` hint syntax with `sp.record`, `sp.variant`, etc.
- [Contract Storage](https://smartpy.tezos.com/manual/syntax/contracts.html): Initialize contract state directly on `self.data` with explicit type casting using `sp.cast`.
- [Entrypoints](https://smartpy.tezos.com/manual/syntax/contracts.html): Define contract entry points using the `@sp.entrypoint` decorator.
- [Private Methods](https://smartpy.tezos.com/manual/syntax/contracts.html): Create private methods with the `@sp.private` decorator.
- [Pattern Matching](https://smartpy.tezos.com/manual/data-types/options-and-variants.html): Use Python's native pattern matching (`match` and `case`) for variants and options.

## SmartPy vs Python

- SmartPy cannot use standard Python libraries within contract code (but they can be used in test code).
- Python f-strings are not supported within contract code (but can be used in test code).
- SmartPy has specific data types related to blockchain operations.
- For records, SmartPy uses the syntax `sp.record(field1=value1, field2=value2)` instead of Python classes.
- For variants, SmartPy uses the syntax `sp.variant("option1", value)` instead of Python enums.

## File Structure and Imports

SmartPy files (`.spy`) can be imported using standard Python import syntax:

```python
# Importing a SmartPy file
import calculator_main as cm

# Importing from a subdirectory
import utils.calculator_main as cm

# Importing from the standard library
import smartpy as sp
import smartpy.math as m
import smartpy.utils as utils
```

SmartPy searches for files based on the filepath in the import statement, looking in:
1. Local inline modules
2. SmartPy files relative to the current working directory
3. SmartPy files in PYTHONPATH directories
4. SmartPy files in site-packages directories

## Code Guide

Here's a complete guide to the new SmartPy syntax:

### Module Definition

SmartPy modules are now Python functions decorated with `@sp.module`.

```python
@sp.module
def my_module():
    pass
```

A module with a type and a contract definition:

```python
@sp.module
def main():
    t_storage: type = sp.record(x=sp.nat)

    class MyContract(sp.Contract):
        def __init__(self):
            self.data.x = 0
            sp.cast(self.data, t_storage)
```

### Type Definitions

Use the `: type` hint to explicitly name types.

```python
@sp.module
def my_module():
    t1: type = sp.unit
    t2: type = sp.nat
    t3: type = sp.int
    t4: type = sp.string
    t5: type = sp.bool
    t6: type = sp.address
    t7: type = sp.bytes
    t8: type = sp.record(field1=t1, field2=t2)
    t9: type = sp.variant(field1=t1, field2=t2)
    t10: type = sp.map[t6, t2]
    t11: type = sp.big_map[t6, t2]
    t12: type = sp.set[t2]
    t13: type = sp.list[t2]
    t14: type = sp.lambda_(t1, t2)
    t15: type = sp.option[t2]
    t16: type = sp.operation
    t17: type = sp.signature
    t18: type = sp.key
    t19: type = sp.key_hash
    t20: type = sp.timestamp
    t21: type = sp.chain_id
    t22: type = sp.bytes
    t23: type = sp.pair[t1, t2]
    t24: type = sp.bool
```

Use the new type system, not the old one with capital 'T'. For example use
`sp.pair[sp.nat, sp.int]` instead of `sp.TPair(sp.TNat, sp.TInt)`.

### Type Casting

Use `sp.cast` for explicit type casting instead of `sp.set_type()` or
`sp.set_type_expr()`.

```python
x = sp.cast({}, sp.map[sp.address, sp.int])
```

### Value Creation

Most types are inferred by default, but explicit definitions may be used to
avoid ambiguity.

```python
a = 1           # inferred as sp.int
b = sp.nat(1)   # explicitly sp.nat
c = sp.cast(1, sp.nat)  # explicitly sp.nat using sp.cast
```

Replace the old syntax for map definition by the new syntax.

```python
# Don't use
x = sp.map(l={}, tkey = sp.TAddress, tvalue = sp.TInt)
y = sp.big_map(l={}, tkey = sp.TAddress, tvalue = sp.TInt)

# Use
x = sp.cast({}, sp.map[sp.address, sp.int])
y = sp.cast(sp.big_map({}), sp.big_map[sp.address, sp.int])
```

### Contract Data Initialization

Contract data is now set directly on `self.data`.

Replace the old `self.init` method by assignments to `self.data`.

```python
# Don't use
self.init(
    field1 = value1,
    field2 = value2,
)

# Use
self.data.field1 = value1
self.data.field2 = value2
```

To specify data types explicitly, use `sp.cast`:

```python
# Don't use
self.init_type(sp.TRecord(field1=sp.TNat, field2=sp.TString))

# Use
sp.cast(self.data, sp.record(field1=sp.nat, field2=sp.string))
```

### Imports

Imports are now done explicitly within modules.

```python
# an inlined SmartPy module
@sp.module
def example():
    def foo():
        pass

# another inlined SmartPy module that uses the previous module
@sp.module
def my_module():
    # To use the `example` module you must import it
    import example
    def bar():
        example.foo()
```

The import only works if the module is visible within the file or if it exists
within a `.spy` that is visible within the Python path.

A way to make the module visible is to import it using a top file import (and
then import it within the module).

For example:

```python
from smartpy.templates import fa2_lib as fa2

# Alias the main template for FA2 contracts
main = fa2.main

@sp.module
def my_module():
    import main

    class MyFungibleContract(
        main.Fungible,
        main.OnchainviewBalanceOf,
    ):
        def __init__(self, contract_metadata, ledger, token_metadata):

            # Initialize on-chain balance view
            main.OnchainviewBalanceOf.__init__(self)

            # Initialize fungible token base class
            main.Fungible.__init__(self, contract_metadata, ledger, token_metadata)
```

### Lambda Definitions

Private lambdas are defined with the `@sp.private` decorator.

```python
@sp.module
def main():
    class C(sp.Contract):
        @sp.private(with_storage="read-only", with_operations=False)
        def multiply(self, a, b):
            return a * b

        @sp.entrypoint
        def ep(self):
            assert self.multiply(a=4, b=5) == 20
```

### Pattern Matching

Use native Python pattern-matching syntax (`match` and `case`).

```python
@sp.module
def main():
    class C(sp.Contract):
        @sp.entrypoint
        def ep(self):
            v = sp.variant.Circle(2)
            match v:
                case Circle(radius):
                    assert radius == 2
                case Rectangle(dimensions):
                    # Do something with dimensions
                    pass
```

You can use `case None` to match the `None` case when dealing with options.

```python
@sp.module
def main():
    class C(sp.Contract):
        @sp.entrypoint
        def ep(self):
            o = sp.Some(5)
            match o:
                case Some(value):
                    assert value == 5
                case None:
                    pass
```

### Entrypoint Assertions

The `check_no_incoming_transfer` parameter has been removed; use explicit
assertions instead.

```python
assert sp.amount == sp.tez(0), "No incoming transfer allowed"
```

### Local Variables

Don't use `sp.local`; instead, define variables directly.

```python
x = value
```

### Verification Statements

Replace `sp.verify()` with Python's native `assert`.

```python
assert x > 5, "X should be superior to 5"
```

### Optional Values

Use `.unwrap_some()` instead of `.open_some()`.

```python
value = optional_value.unwrap_some(error="Value is required")
```

### Flow Control

Replace SmartPy flow controls (`sp.if_()`, `sp.else_()`, `sp.for_()`,
`sp.while_()`) with native Python controls (`if`, `else`, `for`, `while`).

```python
if condition:
    # logic
else:
    # logic

for item in items:
    # logic
```

### Returning Values

Replace `sp.result()` with Python's native `return`.

```python
return result_value
```

Only return if there is not instruction after the block.

### Error Handling

Replace `sp.failwith()` with Python's native `raise`.

```python
raise "X should be superior to 5"
```

### Other Syntax Changes

- `sp.unit` → `()`
- `sp.pair(x, y)` → `(x, y)`
- Prefer `sp.mutez` directly rather than converting `sp.nat` to mutez using utilities like `utils.nat_to_mutez(x)`.

Cursor documentation indexing

To make SmartPy documentation available in Cursor:

  1. Type @docs in the chat
  2. Click + Add new doc
  3. Paste https://smartpy.tezos.com
  4. Name it SmartPy

After adding it, you can use @SmartPy when you want the LLM to search the documentation.

You can find the complete raw Tezos documentation at: https://docs.tezos.com/allPageSourceFiles.txt.