Welcome back to Vyper for Beginners! This lesson covers the basic structure of a Vyper contract, a review of Vyper’s variable types, Vyper’s built-in flow control logic (including how it differs from Python), and wraps up with a simple “Hello World” contract that you can deploy on a test net or a local fork.
This builds off the previous lesson covering the required Vyper toolkit, so please review that if you need a refresher or have not gone through the required installation steps.
Vyper for Beginners
- Part 1 — Introduction
- Part 2 — Toolkit
- Part 3 — Variables, Flow Control, Functions, Hello World!
- Part 4 — Interfaces
- Part 5 — Dynamic Arrays
Variables
Recall that Vyper is a strongly typed high level language. This means that every variable is declared with a type, and every time that variable is manipulated, the compiler will check to ensure that all components of the statement, expression, or operation are valid for that data type.
As a simple example, consider a generic variable named some_number
, which is set to hold only integer values (whole numbers like 1, 2, 3, …, 99, etc.)
If we write a statement that reads some_number = 4
, the compiler will first check that the number 4
is an integer, then it will perform all operations necessary to satisfy it.
Whenever a new variable appears in a Vyper contract, it must be declared with a type. This type may not change during the normal course of execution. In other words, a variable declared with type uint256
can and will never hold anything but a uint256
value. Values stored in variables may be converted between types, but may not be re-assigned to the original variable name. In other words, you may convert a uint256
to a decimal
value, but must store the converted value in a new variable.
Storage, Memory, Calldata
The Ethereum Virtual Machine (EVM) has three different categories for variables:
Storage variables are recorded directly on the blockchain, are mutable, and are declared outside of any function. They are accessed via the self
object, so a storage variable named owner
would be accessible via self.owner
. Modifying these storage variables is expensive.
Memory variables are declared within a function, are mutable, are not recorded on the blockchain, and are discarded after a function returns. They are only visible within the function itself. Reading and modifying these variables is inexpensive, since they do not consume any blockchain storage space.
Calldata variables are not declared inside the function, are immutable, are not recorded on the blockchain, and are discarded after a function returns. They are used to pass data between functions, and to make the function arguments available inside the function itself.
Types
Vyper provides access to variable types of two categories: Value (any use of the variable in a contract will pass its value directly) and Reference (any use of the variable in a contract will pass a reference to an address in memory, instead of the value stored there).
Value Types
- Booleans
bool
(True/False)
- Numbers — Integers
int128
(signed 128-bit integer)
int256
(signed 256-bit integer)
uint8
(unsigned 8-bit integer)
uint256
(unsigned 256-bit integer)
- Numbers — Decimal
decimal
(a signed floating point number with 10 decimal point precision)
- Addresses
address
(20-byte hexadecimal number, with the familiar0x
leading digit
- Byte Arrays
bytes32
(32-byte array, commonly used to store 256-bit keccak hashes)Byte[x]
(a variable-length byte array, wherex
is the length of the maximum byte array that can be stored)
- Strings
String[x]
(a variable-length string byte array that holds alphanumeric strings, wherex
is the length of the maximum string that be stored)
Reference Types
- Lists — may hold a finite number of values that share a common value type. Example:
uint256[4]
is a list that stores four variables of typeuint256
- Dynamic Arrays — similar to lists, except that the array may shrink or grow in size up to the maximum length specified. Example:
DynArray[uint256, 4]
is a dynamic array that may contain between zero and fouruint256
values - Structs — a custom type that consists of two or more differing value types grouped together. Example:
struct wallet
may consist of two internal variables with typesaddress
anduint256
value, and could be used to pass a user’s address and balance into a function that expects them to be paired together. - Mappings — a hash table that allows certain values to be accessed by an associated ‘key’ value. This is most similar to a dictionary in Python. They are declared in the form
HashMap[_keyType _valueType]
where_keyType
is some value type likeuint256
, and_valueType
is another likeaddress
.
NOTE: these variable types are described in great detail in the Vyper documentation, including a list of supported operators, comparisons, and minimum and maximum values.
Default Values
Vyper as no concept of a null
(not set) value. It therefore has a default value for all data types, which can be accessed by calling the empty(type)
function on the appropriate data type. As an example, empty(uint256)
returns 0, empty(address)
returns 0x0000000000000000000000000000000000000000 and empty(bool)
returns False
.
Depending where the variable is first declared within the contract, a variable may or may not require an initial value. For storage variables, no default is required. For memory variables, a default value is required. For calldata variables, the default value is optional.
Public vs. Private
By default, all storage variables are private
, which means they are intended to be directly accessed by functions within the smart contract. In contrast, marking a variable public
will allow anyone to directly view the value stored within that storage slot.
Side note: Storage on the Ethereum blockchain is accessible even when set to private, and you can still access the data inside a storage slot even if the variable is not marked public
. It’s made much easier when you mark a variable as public
, though. To make a variable public, wrap the type inside public
(). Example: some_var: uint256
becomes some_var: public(uint256)
.
By declaring a variable as public
, Vyper will automatically create a “getter” function of the same name, such as some_var()
, that returns the value stored at that location.
Flow Control Statements
Vyper offers only a subset of the flow control options available in Python. Only if
and for
flow control loops are supported (while
is not). The lack of a while
loop may require some careful re-factoring of your usual patterns, but a for
loop with careful use of break
and continue
will allow you the same control over typical iterative operations.
Conditional Execution with if/elif/else
The typical conditional execution patterns from Python are available in Python, including the familiar "and"
, "not"
, and "in"
keywords.
“Truthy” Conversions
A key difference that might trip up Python programmers is that Vyper does not offer any implicit conversion for non-zero values, aka “truthy” comparisons. A typical Python programming pattern (if variable:
) will execute the following statement for variable
values greater than zero. Vyper does not allow this implicit conversion, but the equivalent is only slightly more verbose (if variable > 0:
)
Iteration using for
The for
statement is designed to loop through any iterable structure. You may also use the familiar range()
construct from Python, which provides similar behavior except for the lack of a custom step
value (though there is an open issue on GitHub for this feature).
A “gotcha” that might trip a Python programmer up is that range()
only accepts a literal integer greater than zero. In other words, you cannot use the familiar expression for i in range(len(array_name))
, since len(array_name)
is not a literal. You also cannot do something like…
length = len(array_name)
for i in range(length)
because again, length
is not an integer literal!
The solution here is to define a maximum expected number of executions of this loop, and to break it when you reach the end:
length = len(array_name)
for i in range(999999):
if i == length - 1:
break
else:
[do some other stuff]
This will feel very clunky if you’ve only used Python, but it’s important for EVM since a node must be able to predict gas costs from outside the function before it executes.
Functions
Vyper function declaration is just like Python, with the exception of the requirement to define a return type. Vyper uses the “decorator” formatting scheme to modify the type for all defined functions. The available function types are:
@external
— the function is accessible from external addresses@internal
— the function is inaccessible from external addresses, and only accessible from within this contract@pure
— the function does not access any contract state or variables@view
— the function does not alter any contract state, but may access the contract state@payable
— the function may accept Ether (or the native gas token for this particular blockchain)@nonreentrant(key)
— the function may not be called from an external address if a call is currently in progress (first call must complete and return before it may be called again)
All functions must be declared with at least one visibility decorator (@external
or @internal
), and the rest are optional. These may be combined (unless mutually exclusive), so a function may be have several decorators, such as @external @payable
The functions are defined with this general form:
@decorator1
@decorator2
def function_name(
argument1: type,
argument2: type,
) -> type:
[function logic starts here]
The key difference between a function defined in Vyper vs. Python is the requirement for specifying the return type (syntax -> type
before the colon)
the return type may be omitted if the function does not return. This differs slightly from Python, where you would write -> None
instead. Vyper does not have a None
keyword.
Hello, World!
Every beginning tutorial must include the classic “Hello, World!” example, so here we go! Visit Remix, a browser-based virtual development environment built by the Ethereum team. Using Remix allows you to experiment with smart contracts and simulate their deployments for free inside a fake blockchain that lives inside your browser. It supports both Solidity and Vyper and offers several useful plugins that I encourage you to explore.
Create a new workspace called vyper_hello_world
, then in the File Explorers panel, right-click and delete the three example Solidity contracts (1_Storage.sol
, 2_Owner.sol
, and 3_Ballot.sol
). Then right-click the contracts
directory and create a new file named hello_world.vy
Then paste the following simple contract into the code window:
message: String[24]
@external
def __init__():
self.message = "Hello World!"
@external
@view
def hello() -> String[24]:
return self.message
@external
def change_message(message: String[24]):
self.message = message
Let’s examine the contract:
The first line defines a private storage variable named message
, which stores a String
of up to 24 characters.
The first function definition describes an external function named __init__()
, also known as the constructor. The constructor is executed automatically, and only once, during the contract deployment. Inside the constructor we store the string "Hello World!"
to a variable named self.message
. Recall that message
is a storage variable defined on the first line. We can access any storage variable using dot notation via the self
object.
The second function is an external view function named hello()
that returns the value stored at self.message
.
Then to finish off, we define a third external function called change_message()
that changes the value stored within message
.
With this entered, click the 4th button on the Remix toolbar (the V symbol) to access the Vyper compiler. Choose the remote compiler, then click the blue button marked “Compile contracts/hello_world.vy”.
Then click the 3rd button on the Remix toolbar (the Ethereum logo), click Deploy, then wait for a new entry to appear under “Deployed Contracts”. After it does, click the drop-down arrow and you’ll see two buttons: an orange change_message
and a blue hello
. Clicking the blue hello
button will display 0: string: Hello World!
which means that the function has returned the value stored at storage slot 0 (a string with contents "Hello World"
).
Enter a new message in the string message
field next to the orange change_greeting
button, then click it. You will see a transaction, and then you’ll be able to click the blue hello
button again and see the updated message.
Next steps
In the next lesson I will cover interfaces, the necessary gateway between your contract and other deployed contracts on the blockchain. Using interfaces, you will be able to send and receive information and assets between your smart contract and other addresses.