Pymich types#

Pymich defines some special data structures in order to be able to respect the semantic of the underlying generated Michelson code. Indeed, Michelson lists behave vastly differently than Python lists. Although it would be possible to compile Python lists to Michelson, this could not be done without introducing a significant overhead resulting in much higher gas costs than expected. Instead, the route that Pymich takes is to redefine the basic datastructures provided by Michelson and respect its semantics. These reimplemented data-structures ensure that one gets the expected gas consumptions when compiling down to Michelson while maintaining the same results when ran under CPython and a Michelson VM.

Morever, a second argument in favor of redefining the datatypes is that Michelson data types do not necessarily implement all methods that Python types do. An example of that are lists that, in Python, define an append, whereas its Michelson counterpart do not. Hence, by redefining the data structures, we can expose only the relevant methods, and Python IDEs will be able to typecheck Pymich code and provide proper auto complete.

These data structures are implemented à la Immutable.js, a Javascript library designed by Facebook in order to disallow mutations. The idea is that any datastructure modification returns a new data structure rather than modifies the original one. The implementation of these datastructures can be found here

Before diving into the specifics of each data-type, let us go through an example of how these immutable data structures behave:

from pymich.michelson_types import Nat, Map

michelson_map_a = Map[Nat, Nat]()
michelson_map_a[Nat(1)] = Nat(1)

michelson_map_b = Map[Nat, Map[Nat, Nat]]()
michelson_map_b[Nat(1)] = michelson_map_a

michelson_map_a_copy = michelson_map_b[Nat(1)]
michelson_map_a_copy[Nat(2)] = Nat(2)

michelson_map_a_copy[Nat(2)]  # works
michelson_map_a[Nat(2)]       # throws

Where as using the native Python dictionary would lead to the following behavior:

python_map_a = {1: 1}
python_map_b = {1: python_map_a}
python_map_a_bis = python_map_b[1]
python_map_a_bis[2] = 2

python_map_a_bis[2]  # works
python_map_a[2]      # works as well

Hence, by using the Pymich data structures, we can run our contracts in the CPython interpreter and benefit from all the Python tooling while playing well with the underlying Michelson data structures.

Base types#

Base types are the simplest types possible in Pymich. Notice that all base types are constructed from a constructor in pymich.michelson_types and take a python type as argument. Hence in Pymich, we only use Python strings to construct pymich.michelson_types.String and pymich.michelson_types.Address. Python numbers are used to construct pymich.michelson_types.Nat, pymich.michelson_types.Int, pymich.michelson_types.Mutez and pymich.michelson_types.Timestamp.

from pymich.michelson_types import *

some_address = Address("KT1Ha4yFVeyzw6KRAdkzq6TxDHB97KG4pZe8")
some_string = String("my string")
some_nat = Nat(10)
some_int = Int(-10)
some_mutez = Mutez(42)
some_timestamp = Timestamp(1650053840)

This choice allows for powerful integration into the Python dev tooling ecosystem. For example, using Mypy, one can get proper typechecking right into any editor:

Nat(10) - Nat(15)  # returns an Int
Int(10) + Nat(10)  # returns an Int
Int(10) < Nat(15)  # throws a type error
Nat(10) / Nat(3)   # div operator not allowed
Nat(10) // Nat(3)  # floor div is allowed
len(String("foo")  # returns a String
Int(-1).is_nat()   # returns None
Int(1).is_nat()    # returns Nat(1)

Generic types#

Generic types are data structures that are build from basic types such as lists, maps, big maps, contracts and sets.

Lists#

my_list = List[Nat]()
my_new_list = my_list.prepend(Nat(42))
my_list_length = len(my_list)

# loops
sum = Nat(0)
for el in my_list:
    sum = sum + el

Note

In the near future, list literals will be able to instanciated with List(el1, ..., eln).

Maps#

my_map = Map[Nat, String]()
my_key = Nat(42)
my_map[my_key] = String("The answer.")
my_string = my_map[my_key]
my_second_map = my_map.add(Nat(10), String("Ten"))
foo = Nat(10) in my_second_map  # foo is typed as a boolean

# loops
sum = Nat(0)
for key, value in my_list:
    sum = key + len(value)

Big maps#

my_big_map = BigMap[Nat, String]()
my_key = Nat(42)
my_big_map[my_key] = String("The answer.")
my_string = my_big_map[my_key]
my_second_big_map = my_big_map.add(Nat(10), String("Ten"))
foo = Nat(10) in my_second_map  # foo is typed as a boolean

Sets#

my_big_map = BigMap[Nat, String]()
my_key = Nat(42)
my_big_map[my_key] = String("The answer.")
my_string = my_big_map[my_key]
my_second_big_map = my_big_map.add(Nat(10), String("Ten"))
foo = Nat(10) in my_second_map  # foo is typed as a boolean

Records#

class Student(Record):
    name: String
    age: Nat
    grades: Map[String, Nat]

alice = Student(
    Name("alice"),
    Nat(42),
    Map[String, Nat](),
)
class_code = String("FR101")
alice.grades[class_code] = Nat(15)
french_grade = alice.grades[class_code]

Note

Just like with other data structures, record attribute access returns a copy of its content

Contracts#

@dataclass
class TransferParam:
    _from: Address
    _to: Address
    value: Nat

fa12 = "KT1EwUrkbmGxjiRvmEAa8HLGhjJeRocqVTFi"
transfer_entrypoint = Contract[TransferParam](fa12, "%transfer")

Transactions#

# Transfer to a contract
transaction(
    transfer_entrypoint,
    Mutez(0),
    TransferParam(self.fa12, self.fa12, Nat(10)),
)

# Transfer to an implicit address
transaction(Contract[Unit](SENDER), AMOUNT, Unit)

Grammar#

python_types     ::=
                   | python_number
                   | python_string

base_types       ::=
                   | Unit
                   | String
                   | Nat
                   | Int
                   | Boolean
                   | Address
                   | Bytes

type             ::=
                   | base_type
                   | Operation
                   | Contract
                   | Map[type, type]
                   | BigMap[type, type]
                   | List[type]
                   | Set[type]
                   | Record[attr1=type, ..., attrn=type]

API#

class michelson_types.Address(address: str)#
__eq__(other: Address) bool#

Return self==value.

__hash__() int#

Return hash(self).

class michelson_types.BaseContract(ops: 'Operations' = <michelson_types.Operations object at 0x7f5c17a82610>)#
__eq__(other)#

Return self==value.

__hash__ = None#
class michelson_types.BigMap#

Michelson big map abstraction. It differs with a regular Python dictionary in that it: - is instanciated from literals by deepcopying the literal key/values - adding an element will add a copy of that element - getting an element will return a copy of that element - it is not iterable

__getitem__(key: KeyType) ValueType#

Returns a COPY of the value to retrieve

__setitem__(key: KeyType, value: ValueType) None#

Store a COPY of the value

__str__() str#

Return str(self).

add(key: KeyType, value: ValueType) BigMap[KeyType, ValueType]#

Returns a COPY of self with a COPY of the value added

get(key: KeyType, default: ValueType) ValueType#

Returns a copy of the value

remove(key: KeyType) BigMap[KeyType, ValueType]#

Returns a COPY of the value

class michelson_types.Bytes(value: MichelsonType)#
__eq__(other: Bytes) bool#

Return self==value.

__ge__(other: Bytes) NotImplementedError#

Return self>=value.

__gt__(other: Bytes) NotImplementedError#

Return self>value.

__hash__() int#

Return hash(self).

__le__(other: Bytes) NotImplementedError#

Return self<=value.

__lt__(other: Bytes) NotImplementedError#

Return self<value.

class michelson_types.Contract(address: Address, entrypoint_name: Optional[str] = None)#
class michelson_types.Int(value: int)#
__eq__(other: Int) bool#

Return self==value.

__ge__(other: Int) bool#

Return self>=value.

__gt__(other: Int) bool#

Return self>value.

__hash__() int#

Return hash(self).

__le__(other: Int) bool#

Return self<=value.

__lt__(other: Int) bool#

Return self<value.

class michelson_types.List(*elements: ParameterType)#
__hash__() int#

Return hash(self).

__str__() str#

Return str(self).

class michelson_types.Map#

Michelson big map abstraction. It differs with a regular Python dictionary in that it: - is instanciated from literals by deepcopying the literal key/values - adding an element will add a copy of that element - getting an element will return a copy of that element

__hash__() int#

Return hash(self).

__setitem__(key: KeyType, value: ValueType) None#

Store a COPY of the value

__str__() str#

Return str(self).

add(key: KeyType, value: ValueType) Map[KeyType, ValueType]#

Returns a COPY of self with a COPY of the value added

get(key: KeyType, default: ValueType) ValueType#

Returns a copy of the value

remove(key: KeyType) Map[KeyType, ValueType]#

Returns a COPY of the value

class michelson_types.MichelsonType#

Base type to inherit from to define further types.

class michelson_types.Mutez(amount: int)#
__eq__(other: Mutez) bool#

Return self==value.

__ge__(other: Mutez) bool#

Return self>=value.

__gt__(other: Mutez) bool#

Return self>value.

__hash__() int#

Return hash(self).

__le__(other: Mutez) bool#

Return self<=value.

__lt__(other: Mutez) bool#

Return self<value.

class michelson_types.Nat(value: int)#
__eq__(other: Nat) bool#

Return self==value.

__ge__(other: Nat) bool#

Return self>=value.

__gt__(other: Nat) bool#

Return self>value.

__hash__() int#

Return hash(self).

__le__(other: Nat) bool#

Return self<=value.

__lt__(other: Nat) bool#

Return self<value.

class michelson_types.Operation#
class michelson_types.Option(el: Optional[ParameterType] = None)#
__eq__(other: Option) bool#

Return self==value.

__hash__ = None#
class michelson_types.Record#
__eq__(o: object) bool#

Return self==value.

__hash__() int#

Return hash(self).

__setattr__(_Record__name: str, value: MichelsonType) None#

Implement setattr(self, name, value).

class michelson_types.Set(*args: ParameterType)#

Michelson set abstraction. It differs with a regular Python set in that when instanciating or adding an element to a set, a copy of that element is added rather than the element itself.

__hash__() int#

Return hash(self).

__str__() str#

Return str(self).

add(element: ParameterType) None#

Adds a COPY of the value

remove(element: ParameterType) None#

Removes an element from the set

class michelson_types.String(value: str)#

Michelson string type.

Parameters:

value – value

__add__(other: String) String#

String concatenation

__eq__(other: String) bool#

Return self==value.

__ge__(other: String) bool#

Return self>=value.

__gt__(other: String) bool#

Return self>value.

__hash__() int#

Return hash(self).

__le__(other: String) bool#

Return self<=value.

__lt__(other: String) bool#

Return self<value.

__str__() str#

Return str(self).

class michelson_types.Timestamp(timestamp: int)#
__eq__(other: Timestamp) bool#

Return self==value.

__ge__(other: Timestamp) bool#

Return self>=value.

__gt__(other: Timestamp) bool#

Return self>value.

__hash__() int#

Return hash(self).

__le__(other: Timestamp) bool#

Return self<=value.

__lt__(other: Timestamp) bool#

Return self<value.

__str__() str#

Return str(self).

class michelson_types.Unit#

Michelson Unit type.