Brownie Suite Install and Vyper Programming

Brownie Suite Installation

Brownie is a Python framework for Ethereum development with full support for both Solidity and Vyper. You do not need to install Vyper separately: eth-brownie declares a pinned vyper (and vvm, the Vyper version manager) as a dependency, and it installs and selects the compiler version your contracts need automatically. Put your Vyper sources in contracts/*.vy and Brownie compiles them.

Install Brownie with pipx (not plain pip), so it stays isolated and you do not need a virtualenv:

python3 -m pip install --user pipx
python3 -m pipx ensurepath

pipx does not ship with Python. After ensurepath you may need to open a new terminal before the updated PATH is available. Then install Brownie:

pipx install eth-brownie

To upgrade it later:

pipx upgrade eth-brownie

Brownie uses Ganache as its local development network. Ganache must be installed, but you do not start it by hand — Brownie launches and connects to it automatically each time it loads:

npm install -g ganache

Note that ganache-cli was renamed to ganache, so install the current package.

In an empty directory, initialize a new project and open the console:

mkdir brownietest
cd brownietest
brownie init --force
brownie compile
brownie console

brownie console starts its own Ganache instance for you, so there is no separate ganache step. Use brownie console --network <id> if you want to connect to a different network instead.

Then you can run the script of brownie under scripts/testscript.py with the following command:

brownie run scripts/testscript.py dummycall

A Minimal Vyper Contract

With the project initialized, put a Vyper contract in contracts/GreeterVyper.vy. The # pragma version line is what tells Brownie which compiler to use — it installs the matching Vyper automatically if it is missing:

# pragma version >=0.4.0

owner: public(address)
greeting: public(String[100])


@deploy
def __init__(_greeting: String[100]):
    self.owner = msg.sender
    self.greeting = _greeting


@external
def set_greeting(_greeting: String[100]):
    assert msg.sender == self.owner, "not owner"
    self.greeting = _greeting


@external
@view
def greet() -> String[100]:
    return self.greeting

The owner and greeting state variables are declared public, so Vyper generates getters for them. The @deploy decorator marks the constructor in Vyper 0.4.x (older 0.3.x uses @external def __init__(...) instead).

The Same Contract in Solidity

Brownie compiles both languages side by side, so the identical logic in contracts/Greeter.sol:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Greeter {
    string public greeting;
    address public owner;

    event GreetingChanged(address indexed by, string newGreeting);

    constructor(string memory _greeting) {
        greeting = _greeting;
        owner = msg.sender;
    }

    function setGreeting(string calldata _greeting) external {
        require(msg.sender == owner, "not owner");
        greeting = _greeting;
        emit GreetingChanged(msg.sender, _greeting);
    }
}

Testing Both with pytest

Brownie exposes every contract in your project as a pytest fixture named after the contract, plus built-in fixtures like accounts. Tests live in tests/ and their filenames must start with test_. Reverted transactions are checked with brownie.reverts:

# tests/test_greeter.py
import brownie


def test_vyper_initial_greeting(GreeterVyper, accounts):
    greeter = GreeterVyper.deploy("hello from Vyper", {"from": accounts[0]})
    assert greeter.greeting() == "hello from Vyper"
    assert greeter.owner() == accounts[0].address


def test_vyper_owner_can_update(GreeterVyper, accounts):
    greeter = GreeterVyper.deploy("hello", {"from": accounts[0]})
    greeter.set_greeting("hola", {"from": accounts[0]})
    assert greeter.greet() == "hola"


def test_vyper_non_owner_reverts(GreeterVyper, accounts):
    greeter = GreeterVyper.deploy("hello", {"from": accounts[0]})
    with brownie.reverts("not owner"):
        greeter.set_greeting("nope", {"from": accounts[1]})


def test_solidity_initial_greeting(Greeter, accounts):
    greeter = Greeter.deploy("hello from Solidity", {"from": accounts[0]})
    assert greeter.greeting() == "hello from Solidity"
    assert greeter.owner() == accounts[0].address


def test_solidity_owner_can_update(Greeter, accounts):
    greeter = Greeter.deploy("hello", {"from": accounts[0]})
    greeter.setGreeting("hola", {"from": accounts[0]})
    assert greeter.greeting() == "hola"


def test_solidity_non_owner_reverts(Greeter, accounts):
    greeter = Greeter.deploy("hello", {"from": accounts[0]})
    with brownie.reverts("not owner"):
        greeter.setGreeting("nope", {"from": accounts[1]})

brownie test runs the suite; brownie test --coverage adds a coverage report and brownie test -n auto parallelizes it.

A Deploy Script

Deployment scripts are plain Python functions. Brownie hands them the accounts container, and each call that changes state needs a {"from": ...} sender:

# scripts/deploy.py
from brownie import Greeter, GreeterVyper, accounts


def main():
    owner = accounts[0]
    Greeter.deploy("hello from Solidity", {"from": owner})
    GreeterVyper.deploy("hello from Vyper", {"from": owner})

The finished project looks like this:

brownietest/
├─ contracts/
│  ├─ Greeter.sol
│  └─ GreeterVyper.vy
├─ scripts/
│  └─ deploy.py
└─ tests/
   └─ test_greeter.py

And the three commands you will use every day:

brownie compile                    # compiles both contracts
brownie test                       # runs the pytest suite
brownie run scripts/deploy.py      # deploys to the local Ganache network
Tags: Writing

Comments

Leave a comment

What is 2 + 3?
Please solve the arithmetic question above to verify you're human.