The Complete Web 3 Python Stack - Part 1 (Vyper, Ape, Web3.py)

The Complete Web 3 Python Stack - Part 1 (Vyper, Ape, Web3.py)


💡 I’ve been dying to write this series. As a Pythonista, this is just…chefkiss

If you don't want to read, but watch a video CLICK HERE

OK. You’re a Pythonista. You’re in web3 or looking at web3. You want to use them Python skills. Lets get started.

In this article we’re going to install and setup Vyper (smart contract language), Ape (development framework and Web3.py(interact with smart contract). This is the full pythonic web3 stack.

Click here to fetch the Github repo.

Setup your virtual environment

As pythonista’s we like to keep a clean pc. Let’s spin up a virtual environment.

python3 -m venv ./myv

Activate the virtual environment by running:

source ./myv/bin/activate

Install Dependencies

Cool we got three things to install: Vyper, Ape and web3.py

pip install vyper eth-ape web3

awesome.

One more thing. Ape works with plugins. So its very bare-bone. Depending on your smart contract language, and the network you can want to connect, you can install plugins. We’re going to keep it simple for now. We’re using vyper, so install pip install ape-vyper. More info, click here.

Build an Ape project

So next up inside any folder you want, create an ape project. First lets create and navigate to a new folder

mkdir hello_world
cd hello_world

Then use the following command to create a new ape project

ape init

You will be prompted for a name for your project. I named mine hello

Write you Vyper contract

So Vyper is pythonic , which means it’s very similar to python, with a few differences in syntax and the way the language operates. Rest assured, these are small adjustments. Let’s start nice and easy with a hello world contract. It has a variable with hello world and one function to set the value of the variable when we deploy the contract.

# Define the version
# @version ^0.3.0

# Create a string variable that can store maximum 100 characters
greet: public(String[100])

#Define the visibility with a decorator
@external

#This is the constructor that sets the value of the variable greet 
#upon deploying the smart contract
def __init__():
    self.greet = "Hello World"

Before we compile and deploy, let’s take a closer look.

Variable

First is our variable. The structure of defining a variable is as follows:

:()

So in our case we have a string named greet . Anyone can see this variable once we deploy our contract on chain, because it has the visibility public.

Strings

Now…strings are special in Vyper. The number 100 defined inside our string refers to the amount of characters we can store in there. This only applies to strings. Here’s an example when you define a number or a boolean.

fav_num: public(uint256)
flag:public(bool)

The decorator

So what is this @external that we’re using. Well above every function we can use a decorator (just like you’re used too in Python). In Vyper there 5 pre-defined decorator for functions:

public functions can be called internally and via transactions,

external functions can only be called via transactions and not internally,

internal functions can only be called within the contract or its derivatives and not via transactions,

view functions are read-only and do not modify the state of the blockchain,

and pure functions are a special case where they promise not to modify or read the contract's state.

In our case we’re using the @external for our constructor, but what is a constructor. Next chapter.

Constructors in Vyper

If you’re coming from Solidity, you’re familiar with constructors, just the syntax is different.

For newcomers, the constructor is a special function that only runs when a contract is being deployed. That’s it. After that no one can call it.

Constructors are typically handy for setting the owner of the contract, or pre-defining a token supply and a bunch of other stuff.

A constructor is always typed like this:

def __init__():
    ...

The ‘self’ keyword

You’ve seen this before in Python. Self refers to the instance of the class. Over here it refers to the contract. So when setting the value of a global or state variable, we write it out like this:

self.greet = "Hello World"

Cool now lets compile and deploy.

Create an account

When we deploy, we need to pass our private keys. There are a couple of ways to do it:

  1. Pass your private keys through the terminal - Only do this with dummy accounts

  2. Use a .env file - Only use this for dev environments

  3. Encrypt your key through the terminal - Best practise

Now, the risk with a .env file is that you might commit it to Github and then your private key becomes public. We don’t want that.

Best practise is to pass it through the terminal and encrypt it. Ape allows this natively, so we’re going to use this method. Run the following command to get a list of accounts

ape accounts list

run the following command to create a new account

ape accounts import <NAME>

So as an example

ape accounts import scrollsepolia

You will be prompted to pass your private key and then a password. Don’t forget this password (duhh). Run ape accounts -list to double check your account got added.

Compile and Deploy

First we need to compile our contract. Make sure you have the ape-vyper plugin installed. Run:

ape compile

This generate a file in the .build necessary to deploy our contract

Deploy script

Now we can deploy directly from the CLI, but it’s better to do it with a script. We have more control and it’s best practise.

from ape import accounts, project

def main(): 
      # Initialize deployer account and print balance 
    dev_account = accounts.load("scrollsep") 
    print(f'The account balance is: {dev_account.balance / 1e18} ETH')  
     # Deploy the smart contract and print a message 
    kw = {
        'type': 0
    }
    dev_account.deploy(project.hello, **kw) 
    print("Contract deployed!")

The only thing I want to highlight, is this part

kw = {
        'type': 0
    }

We’re passing a keyword argument with type:0. Now…there is a reason. For some chains running it like this will work:

dev_account.deploy(project.hello)

So without the keyword argument. The reason we’re passing the kw, is due to the fact that some chains do not support EIP 1559 transactions. There are different type of transactions. If you’re curious, click here. If you believe me, go ahead and continue.

Run the following command to deploy your smart contract

ape run scripts/deploy.py --network <RPC URL>

Here’s my example deploying it to Scroll Sepolia test-net

ape run scripts/deploy.py --network https://sepolia-rpc.scroll.io/

Now we could further optimise, by preventing to pass an rpc-url directly. We’re going to do this in the next article.

You will be asked if you want to sign the transaction, say yes. Then pass your password to unlock the account and you will be asked if you want to leave your account unlocked. I always say no.

Done!

Interact with your smart contract

We deployed our smart contract. Great. Now we can use web3.py to interact with our smart contract. Go ahead and create a file called hello_contract.py. This is the full code of our contract:

# Importing necessary libraries
import json            # Importing the json library for handling JSON data
from web3 import Web3  # Importing Web3 from the web3.py library

# Specify the URL of the blockchain node you want to connect to
rpc_url = "https://sepolia-rpc.scroll.io/"
# Creating a Web3 instance and connecting to the Ethereum blockchain using the specified node
web3 = Web3(Web3.HTTPProvider(rpc_url))

# ABI (Application Binary Interface) for the smart contract
# This is a JSON string that describes how to interact with the contract
abi = json.loads('[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"greet","outputs":[{"name":"","type":"string"}],"stateMutability":"view","type":"function"}]')

# The Ethereum address of the deployed smart contract
address = "0x268d4A9c3Ba20C938B9f3Db634786aB93628A7Cb"

# Creating a contract object using the Web3 instance, contract address, and ABI
# This object will be used to interact with the smart contract
contract = web3.eth.contract(address=address, abi=abi)

# Calling the 'greet' function of the contract
# Since 'greet' is a public variable, it can be accessed as a function
# This call does not make any changes to the blockchain, it just reads data
greet_value = contract.functions.greet().call()

# Printing the value obtained from the smart contract
print("Greet value:", greet_value)

In your directory .build inside a .json file you can find the abi. My contract is named hello so the file is named hello.json.

This json has one huge object (everything between squarely brackets), you need only the abi part. This is what mine looks like:

abi = json.loads('[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"greet","outputs":[{"name":"","type":"string"}],"stateMutability":"view","type":"function"}]')

Check out the Github, if you want to know what exact part I took from my hello.json.

In your terminal, you can find your smart contract address. You will need to pass that here

# The Ethereum address of the deployed smart contract
address = "0x268d4A9c3Ba20C938B9f3Db634786aB93628A7Cb"

Our variable greet has a visibility of public , so it’s call-able, like this:

greet_value = contract.functions.greet().call()

To run this script, in your terminal run

python hello_contract.py

The output should look something like this

Greet value: Hello World

Bonus

Now…if you’re like me and your terminal is sometimes just a bit of a mess and you just want your contract address written somewhere. Then I got some good news. You can adjust your deploy script to look like this

from ape import accounts, project
from datetime import datetime
def main(): 
    contract_name = "hello"
      # Initialize deployer account and print balance 
    dev_account = accounts.load("scrollsep") 
    print(f'The account balance is: {dev_account.balance / 1e18} ETH')  
     # Deploy the smart contract and print a message 
    kw = {
        'type': 0
    }
    contract_class = getattr(project, contract_name)
    deployed_contract = dev_account.deploy(contract_class, **kw)
    print("Contract deployed!") 

        contract_address = deployed_contract.address
    # Get the current timestamp
    current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    # Write the contract name, address, and timestamp to a file
    with open("deployed_contracts.txt", "a") as file:
        file.write(f"{current_time}: {contract_name} - Address: {contract_address}\n")

    print(f"{contract_name} address {contract_address} recorded with timestamp.")

We define the contract name first at the top of the deploy script

contract_name = "hello"

Then you need to use the getattr function to dynamically access an attribute (in this case, the contract) based on a string variable.

contract_class = getattr(project, contract_name)

Now we can deploy the contract using the following line

deployed_contract = dev_account.deploy(contract_class, **kw)

Here’s where we write the name, date and contract address to a text file

 # Write the contract name, address, and timestamp to a file
    with open("deployed_contracts.txt", "a") as file:
        file.write(f"{current_time}: {contract_name} - Address: {contract_address}\n")

Done!

Dang…you’ve created a virtual environment, installed & configured Ape, wrote a smart contract in Vyper and interacted with it using web3.py. Mission accomplished!

Now that we have this fundamental basis, we can use this to build more awesome stuff. Stay tuned for the next one!