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
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.
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).
- 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)
address(20-byte hexadecimal number, with the familiar
- Byte Arrays
bytes32(32-byte array, commonly used to store 256-bit keccak hashes)
Byte[x](a variable-length byte array, where
xis the length of the maximum byte array that can be stored)
String[x](a variable-length string byte array that holds alphanumeric strings, where
xis the length of the maximum string that be stored)
- Lists — may hold a finite number of values that share a common value type. Example:
uint256is a list that stores four variables of type
- 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 four
- Structs — a custom type that consists of two or more differing value types grouped together. Example:
struct walletmay consist of two internal variables with types
uint256value, 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
_keyTypeis some value type like
_valueTypeis another like
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.
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
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
some_var: uint256 becomes
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
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
continue will allow you the same control over typical iterative operations.
Conditional Execution with
The typical conditional execution patterns from Python are available in Python, including the familiar
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:)
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)
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.
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 (
@internal), and the rest are optional. These may be combined (unless mutually exclusive), so a function may be have several decorators, such as
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
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 (
3_Ballot.sol). Then right-click the
contracts directory and create a new file named
Then paste the following simple contract into the code window:
message: String @external def __init__(): self.message = "Hello World!" @external @view def hello() -> String: return self.message @external def change_message(message: String): 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
The second function is an external view function named
hello() that returns the value stored at
Then to finish off, we define a third external function called
change_message() that changes the value stored within
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
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.
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.