Brownie – Smart Contracts in Python

This is an introductory article to Brownie, a smart contract development and testing framework for Solidity and Vyper, especially with Python developers in mind. 

We first start with the installation process and then create a project with a simple smart contract. We will look at how to interact with the smart contract on a local blockchain using the built-in console. Finally, we will look at how to run a unit test.

What is Brownie?

Brownie is a popular smart contract development and testing framework for the Ethereum Virtual Machine, supporting Solidity and Vyper as the smart contract languages.

It is Python-based, meaning that it uses various Python libraries, such as web3.py and pytest, and uses Python to write scripts. It also has a built-in console similar to the Python interpreter to interact with smart contracts.

All in all, if you are a Python developer and a fan of Pythonic style, you will probably feel more comfortable using Brownie than other JavaScript-based frameworks, such as Truffle or Hardhat.

How to Install Brownie?

The Brownie documentation recommends to use pipx to install Brownie. So, let’s install pipx first unless you already have it on your machine.

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

Then, install Brownie.

$ pipx install eth-brownie
  installed package eth-brownie 1.17.1, Python 3.9.1
  These apps are now globally available
    - brownie
done! ✨ 🌟 ✨

We can check that Brownie has been installed successfully by running the brownie command:

$ brownie --version
Brownie v1.17.1 - Python development framework for Ethereum

How to use Brownie

In this section, we will look at Brownie’s basic functionality, such as:

  • Create a project from scratch
  • Use the console to interact with the smart contract
  • Run unit tests 

How to create a Brownie project

To create a project, run the command brownie init in an empty directory. Brownie has a template system called Brownie mixes, which we can use to create a project for specific purposes, such as ERC-20 token, NFT, etc. But in this article, we will start from an empty project and create a very simple smart contract so that we can understand the basic functionality better. 

In the following example, we create a new directory called brownie_test in the home directory and run brownie init inside the new directory.

[~]$ mkdir brownie_test
[~]$ cd brownie_test
[~/brownie_test]$ brownie init
Brownie v1.17.1 - Python development framework for Ethereum

SUCCESS: A new Brownie project has been initialized at /Users/mikio/brownie_test

The command creates the default directory structure, as shown below.

[~/brownie_test]$ tree .
.
β”œβ”€β”€ build
β”‚   β”œβ”€β”€ contracts
β”‚   β”œβ”€β”€ deployments
β”‚   └── interfaces
β”œβ”€β”€ contracts
β”œβ”€β”€ interfaces
β”œβ”€β”€ reports
β”œβ”€β”€ scripts
└── tests

How to compile a smart contract

First, we need a smart contract. Let’s take an example from the Solidity documentation. Save this smart contract in the contracts directory as storage.sol

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract SimpleStorage {
    uint storedData;

    function set(uint x) public {
        storedData = x;
    }

    function get() public view returns (uint) {
        return storedData;
    }
}

We can run the brownie compile command to compile the smart contract.

[~/brownie_test]$ brownie compile
Brownie v1.17.1 - Python development framework for Ethereum

Compiling contracts...
  Solc version: 0.8.10
  Optimizer: Enabled  Runs: 200
  EVM Version: Istanbul
Generating build data...
 - SimpleStorage

Project has been compiled. Build artifacts saved at /Users/mikio/brownie_test/build/contracts

Please note the name of the smart contract (SimpleStorage) because we will need it in the next section.

Brownie automatically compiles smart contracts (if there are any changes) when starting the console or running tests, so we don’t usually need to run the brownie compile command unless we want to compile the code manually.

How to use the Brownie console

Brownie offers the built-in console to interact with the local blockchain and smart contracts, like executing Python code using the Python interpreter. 

Let’s start the console by running the brownie console command.

[~/brownie_test]$ brownie console
Brownie v1.17.1 - Python development framework for Ethereum

BrownieTestProject is the active project.

Launching 'ganache-cli --port 8545 --gasLimit 12000000 --accounts 10 --hardfork istanbul --mnemonic brownie'...
Brownie environment is ready.
>>>

If you are a Python developer, the prompt >>> should be familiar to you. 

In the command output, we can also see that it automatically starts a local blockchain (Ethereum simulator) using Ganache CLI. It creates 10 test accounts by default, which we can access via the object accounts.

>>> accounts
[<Account '0x66aB6D9362d4F35596279692F0251Db635165871'>, <Account '0x33A4622B82D4c04a53e170c638B944ce27cffce3'>, <Account '0x0063046686E46Dc6F15918b61AE2B121458534a5'>, <Account '0x21b42413bA931038f35e7A5224FaDb065d297Ba3'>, <Account '0x46C0a5326E643E4f71D3149d50B48216e174Ae84'>, <Account '0x807c47A89F720fe4Ee9b8343c286Fc886f43191b'>, <Account '0x844ec86426F076647A5362706a04570A5965473B'>, <Account '0x23BB2Bb6c340D4C91cAa478EdF6593fC5c4a6d4B'>, <Account '0xA868bC7c1AF08B8831795FAC946025557369F69C'>, <Account '0x1CEE82EEd89Bd5Be5bf2507a92a755dcF1D8e8dc'>]

We can access each account just like a Python list. For example, the first account is accounts[0], the second account is accounts[1], etc.

>>> accounts[0]
<Account '0x66aB6D9362d4F35596279692F0251Db635165871'>
>>> accounts[1]
<Account '0x33A4622B82D4c04a53e170c638B944ce27cffce3'>

We can see the Ether balance of each account by using the method balance() as shown below.

>>> accounts[0].balance()
100000000000000000000
>>> web3.fromWei(accounts[0].balance(), 'ether')
Decimal('100')

We can access the smart contract we compiled in the previous section by the smart contract name (SimpleStorage).

>>> SimpleStorage
[]

But to use this smart contract, we first need to deploy it using the deploy method. The following example uses the first account (accounts[0]) to deploy the smart contract.

>>> simple_storage = SimpleStorage.deploy({'from': accounts[0]})
Transaction sent: 0xce078c4b3d092d945a56a9b3991e6c9a3a9b712e6ab88dafbcbfe03f1f714f99
  Gas price: 0.0 gwei   Gas limit: 12000000   Nonce: 0
  SimpleStorage.constructor confirmed   Block: 1   Gas used: 90539 (0.75%)
  SimpleStorage deployed at: 0x3194cBDC3dbcd3E11a07892e7bA5c3394048Cc87

The deploy method returns a Contract object.

>>> simple_storage
<SimpleStorage Contract '0x3194cBDC3dbcd3E11a07892e7bA5c3394048Cc87'>

We can now run the functions in the smart contract. For example, let’s call the function get() to check the current storedData value.

>>> simple_storage.get()
0

Then, we can send a transaction to execute the function set() to update the storedData value, for example, to 5.

>>> tx = simple_storage.set(5)
Transaction sent: 0x74b1553a551c617d9c631fe4c92e0144faeaa8df426a38ab2980ac02231a26af
  Gas price: 0.0 gwei   Gas limit: 12000000   Nonce: 1
  SimpleStorage.set confirmed   Block: 2   Gas used: 41394 (0.34%)

The return value is a Transaction object, and we can find more details using the method info().

>>> tx.info()
Transaction was Mined 
---------------------
Tx Hash: 0x74b1553a551c617d9c631fe4c92e0144faeaa8df426a38ab2980ac02231a26af
From: 0x66aB6D9362d4F35596279692F0251Db635165871
To: 0x3194cBDC3dbcd3E11a07892e7bA5c3394048Cc87
Value: 0
Function: SimpleStorage.set
Block: 2
Gas Used: 41394 / 12000000 (0.3%)

We can check the storedData value by calling the function get() again.

>>> simple_storage.get()
5

We can exit the console by running quit(), just like the Python interpreter.

>>> quit()
Terminating local RPC client...

How to run tests in Brownie

Brownie uses pytest to make unit tests more accessible. Let’s create a simple test file test_storage.py in the directory tests.

# File: test_storage.py #

def test_storage(SimpleStoage, accounts):
    simple_storage = SimpleStorage.deploy({'from': accounts[0]})

    assert simple_storage.get() == 0
    simple_storage.set(5)
    assert simple_storage.get() == 5

The test file is a pytest file, which means the usual pytest conventions apply, such as:

  • The test file names start with test_Β 
  • The test function names start with test_.Β 
  • Assertions are done by the assert statement

Brownie automatically creates a fixture for our smart contract (SimpleStorage) and the account object (accounts). So, we can use them by specifying the fixture names (SimpleStorage, accounts) in the function arguments.

If you’re not familiar with pytest, you might find the following articles helpful:

Then, we deploy the contract and execute the functions, as we did on the Brownie console in the previous section. In this test, we first assert that the storedData value is 0. Then, after setting the value to 5 by executing the function set(5), assert that the value has been changed to 5.Β 

Now, go back to the console and run the command brownie test.

[~/brownie_test]$ brownie test   
Brownie v1.17.1 - Python development framework for Ethereum

==================== test session starts ====================
platform darwin -- Python 3.9.1, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: /Users/mikio/brownie_test
plugins: eth-brownie-1.17.1, web3-5.24.0, hypothesis-6.24.0, xdist-1.34.0, forked-1.3.0
collected 1 item                                            

Launching 'ganache-cli --port 8545 --gasLimit 12000000 --accounts 10 --hardfork istanbul --mnemonic brownie'...

tests/test_storage.py .                               [100%]

===================== 1 passed in 2.21s =====================
Terminating local RPC client...

We can see the pytest output, which shows that the test has passed. Like pytest, using the -v option adds more information to the output.

[~/brownie_test]$ brownie test -v
Brownie v1.17.1 - Python development framework for Ethereum

==================== test session starts ====================
platform darwin -- Python 3.9.1, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 -- /Users/mikio/.local/pipx/venvs/eth-brownie/bin/python
cachedir: .pytest_cache
hypothesis profile 'brownie-verbose' -> verbosity=2, deadline=None, max_examples=50, stateful_step_count=10, report_multiple_bugs=False, database=DirectoryBasedExampleDatabase(PosixPath('/Users/mikio/.brownie/hypothesis'))
rootdir: /Users/mikio/brownie_test
plugins: eth-brownie-1.17.1, web3-5.24.0, hypothesis-6.24.0, xdist-1.34.0, forked-1.3.0
collected 1 item                                            

Launching 'ganache-cli --port 8545 --gasLimit 12000000 --accounts 10 --hardfork istanbul --mnemonic brownie'...

tests/test_storage.py::test_storage PASSED            [100%]

===================== 1 passed in 2.33s =====================
Terminating local RPC client...

Summary

In this article, we looked at the basics of Brownie, a popular Python-based smart contract development and testing framework for Solidity and Vyper.

We first looked at how to install Brownie and then created a new project from scratch. We then looked at how to interact with the smart contract on the local blockchain using the console. Finally, we leant how to run unit tests.

The next step would be to deploy the smart contract to a public testnet, but it will be covered in a future article. 

I hope this article has been helpful to you. You can find more information about Brownie in the Brownie documentation.


Learn Solidity Course

Solidity is the programming language of the future.

It gives you the rare and sought-after superpower to program against the “Internet Computer”, i.e., against decentralized Blockchains such as Ethereum, Binance Smart Chain, Ethereum Classic, Tron, and Avalanche – to mention just a few Blockchain infrastructures that support Solidity.

In particular, Solidity allows you to create smart contracts, i.e., pieces of code that automatically execute on specific conditions in a completely decentralized environment. For example, smart contracts empower you to create your own decentralized autonomous organizations (DAOs) that run on Blockchains without being subject to centralized control.

NFTs, DeFi, DAOs, and Blockchain-based games are all based on smart contracts.

This course is a simple, low-friction introduction to creating your first smart contract using the Remix IDE on the Ethereum testnet – without fluff, significant upfront costs to purchase ETH, or unnecessary complexity.