Seahorse is beta software. Many features are unimplemented and it's not production-ready.


Using Seahorse

The Seahorse language

Seahorse is based on Python 3, but only supports a subset of the full Python language. It has some additional constraints on what is and isn't allowed.

Limitations: a brief overview

This first release of Seahorse is a beta/work-in-progress; it comes with a lot of limitations that you wouldn't encounter while writing regular Python code. It's still functional, but doesn't offer the full Python experience yet.

Python featureLimitations
listLists can't be constructed right now, but you can use the Seahorse Array type, which is a like a fix-sized list.
other collectionsDicts and sets can't be constructed right now either
numbersSeahorse only officially supports its own numeric types, which map to underlying Rust types. Floats work mostly the same (under their new name f64), but there are no bigints.
importsSeahorse can only compile a single file at a time, and has no concept of a module system or import statements
classesClasses in Seahorse can only define their fields, and they can't have any associated methods (like constructors)
builtins/std. librarySee below for the list of Python builtins that Seahorse supports

Additionally, there are a couple of constraints on regular statements:

  • Empty.init() can only be called at the start of an instruction
  • SPL token transfers/mints/burns cannot happen outside of an instruction

The Seahorse prelude

When you first create a Seahorse project, a Python file called is imported via from seahorse.prelude import *. This file contains class/function definitions for everything built in to Seahorse, and is used to provide editors with autocompletion and serve as documentation for the things you can do with Seahorse. The following table briefly summarizes the classes/functions that are made available - check for more details:

u8, u64, i64, f64Simple numeric types - a 1-byte unsigned integer, an 8-byte unsigned integer, an 8-byte signed integer, and a floating point number, respectively. They map to Rust builtin types of the same names. Includes some functions to convert between types, since Seahorse cannot always do this automatically.
Array[T, N]Fixed-length array, like a Python list but with a size. N must be an integer literal. Can be created from a list comprehension.
PubkeyA 32-byte public key.
Account, Signer, Empty, TokenAccountTypes for supported Solana accounts. Discussed in detail here.

Python builtins

Only a subset of Python's builtins are supported. Seahorse is actively trying to support as much of Python as possible, but adding support for Rust code generation can be a slow process. The following table summarizes important builtins that can be used in Seahorse:

printLog messages to Solana: print('account:', my_account, 'key:', signer.key())
rangeIterate over a numeric range: for i in range(10): ...
absGet the absolute value of a number: abs(x)
powGet the value of a number raised to a power: pow(base, exp)
min, maxGet the min/max of any amount of numbers: m = max(x, y, z, 0). Coerces to the loosest type of all arguments.
round, floor, ceilf64-to-integer conversions. round() only rounds to integers, you cannot provide a decimal precision level like in Python. The returned type will still be an f64, so you might need to manually cast afterward - e.g., i = u64(round(f)). This is the preferred way to convert an f64 to an integer type.
sortedGet a copy of an array in ascending-order sorted form: sorted(array)
strGet a string representation of an object: str(obj)
sumGet the sum of an array of numbers: sum(array)

Scripts vs. modules

In a Python script, every top-level statement is run in sequence, and the result of the script is just whatever happens during those statements. If you use the script as a module, then everything you define - variables, functions, and classes - are available for import by whoever uses your module.

In Seahorse, there is no concept of a script - the code you write is used to generate a Rust library, which is analagous to a Python module with some extra limitations. Statements are not run unless they are part of a function that gets called - these are your @instruction definitions. The following table summarizes what can do in your Seahorse file as a top-level statement:

ImportsImports for Seahorse-supported libraries (importing unsupported libraries has no effect)
Class definitionsArbitrary classes
Instruction definitionsFunctions decorated with @instruction
declare_id('...')A special statement that tells Anchor what your program's ID is

Type hints and static typing

In Python, you can provide optional type hints on variables assignments, class fields, and function parameters. These hints are completely ignored by Python when your code runs, but your editor might make use of them to allow autocomplete and other features that rely on knowing the types of objects.

In Seahorse, type hints are occasionally mandatory in order to allow the underlying Rust code to be statically typed - that is, typed at compile time. The following table summarizes when you have to (or might want to) use type hints:

LocationNeeds type hints?
Class fieldsALWAYS, unless the class is an Enum.
Function parametersALWAYS.
Variable assignmentMAYBE, Rust has a powerful type inference system that can usually fill in the type of a variable when it is declared. If this isn't enough, you can provide a type when assigning a variable (var: Type = value) and Seahorse will insert code to coerce the value to the proper type.

Using Seahorse, most of your variables can be automatically typed as long as your class fields and function parameters are. Sometimes it might fail and you'll have to add a manual type or two, but for the most part you can rely on it to get you the result you expect.

Numbers and math

Rust has much stricter rules for doing simple math operations than Python - you can't add two different types of numbers, even if the only difference between them is their size (e.g. u8 vs. u64).

Seahorse needs to preserve this but also aims to allow some flexibility for performing math. The numeric coercion rule is simple: (most) mathematical operations will ensure the types of both operands are the same by coercing them to the less strict of the two types. In practice, this means that integers will coerce to floating point numbers, and never the other way around. (This is essentially what Python does with math between ints and floats, but applied to more types.)

The only special operation is division (/) - this will always coerce to the floating point type (f64). If you want to preserve the type of your operands, use the floor division operator instead (//).

a = 1          # u8:  Literal numbers are typed as the strictest type they fit into
b = 1000       # u64: 1000 doesn't fit into a u8, so f is a u64
c: u64 = 1     # u64: Giving g a type manually will make sure it's a u64

d = u64(1)     # u64: Using the constructor always gives you what you expect
e = f64(2.0)   # f64: ^

f = d + e      # f64: Coerces a to an f64, then does the math and returns an f64
g: i64 = d + e # i64: The math is performed as before, then coerced to an i64 after
h = a / 3      # f64: Division always gives a floating-point result
i = a // 3     # u64: Floor division will coerce 3 to a u64, returning a u64

Only a subset of Rust's numeric types are supported. In decreasing order of strictness, these are:

  1. u8 - 1-byte unsigned integer
  2. u64 - 8-byte unsigned integer
  3. i64 - 8-byte signed integer
  4. f64 - 8-byte floating-point number