Guided walkthrough: write Greeter, line by line#

← Back to Week 4: Track exposure

project/README.md's Step 2 explains Greeter.vy piece by piece, but it's explaining a file that's already sitting there, finished. This exercise has you type the same contract yourself, in small pieces, checking that it compiles after each one, before you read that explanation.

Do this first. Create a new file at ../project/contracts/GreeterPractice.vy and type each step below into it, running ape compile from inside the project/ folder after every step. Once you're done, project/README.md's walkthrough of the real Greeter.vy becomes a second pass over material you've already typed once yourself.

A quick note on why the filename has to be different: Vyper contracts have no in-file name the way Solidity's contract Greeter { ... } line gives one. Every .vy file simply is the one contract it defines, and Ape derives that contract's name from the filename itself. That's exactly why this practice file needs its own filename, GreeterPractice.vy; naming happens at the file level in Vyper, not inside the file, so a different filename is what lets both files exist side by side in the same Ape project without colliding.

Step 1: the header#

# SPDX-License-Identifier: MIT
#pragma version ^0.4.0

The first line is a license marker Vyper tooling expects. The second pins the compiler version this file targets, written as a specially recognized comment (note the # prefix) rather than its own keyword the way Solidity's pragma is. Save the file and run ape compile. Nothing meaningful can compile yet (there's no state or functions declared), and Ape may say so; that's expected at this step, you're just confirming the file is being picked up at all.

Step 2: the state variable (no contract wrapper needed)#

greeting: String[280]

There's no Step 2: the contract heading to match here the way the Solidity version has one, and that's the point: Vyper has no contract keyword at all. greeting: String[280] declares a state variable directly at the top level of the file, data that will live permanently in this contract's own storage once deployed, not just for the length of one function call. String[280] is a bounded string type; Vyper requires every string to declare a maximum length in bytes, and enforces it automatically at runtime, unlike Solidity's unbounded string. Run ape compile again. This should compile cleanly now; a file with one state variable and nothing else is completely valid Vyper, even though it doesn't do anything useful yet.

Step 3: an event#

Add this block after greeting:

event GreetingChanged:
    changed_by: indexed(address)
    old_greeting: String[280]
    new_greeting: String[280]

An event is a log entry a transaction can emit, cheap to write and specifically designed to be watchable from outside the contract; notes/04-smart-contracts.md and notes/02-transactions-and-gas.md cover why that matters. A Vyper event is its own indented block, one field per line, instead of Solidity's single-line parameter list. Compile again. Declaring an event that's never emitted yet is also completely valid; you'll use it in Step 5.

Step 4: the constructor#

@deploy
def __init__(initial_greeting: String[280]):
    self.greeting = initial_greeting

The constructor runs exactly once, at the moment of deployment, and never again. @deploy is Vyper's equivalent of Solidity's constructor(...) keyword, a decorator rather than a named function form. initial_greeting needs no memory keyword the way Solidity's parameters do; Vyper doesn't expose a separate memory/storage location system at this level. It only becomes part of permanent storage once you explicitly assign it into self.greeting, the same as any other state-changing write. Compile again.

Step 5: the two functions#

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

@external
def set_greeting(new_greeting: String[280]):
    old_greeting: String[280] = self.greeting
    self.greeting = new_greeting
    log GreetingChanged(old_greeting=old_greeting, new_greeting=new_greeting, changed_by=msg.sender)

@view marks get_greeting as read-only: it costs no gas and needs no signed transaction, because nothing on-chain changes. @external marks a function as callable from outside the contract at all; Vyper requires this explicit decorator on every function, where Solidity's public keyword does that job as part of the function signature itself. set_greeting has no @view, because it does change stored data, so calling it for real requires a signed, gas-paying transaction. msg.sender is filled in automatically by the network as whoever actually signed that transaction; nothing in this function trusts a caller to say who they are. log GreetingChanged(...) is how Vyper actually emits an event: the log keyword is required, and every field is passed by keyword, matching the event's field names exactly, unlike Solidity's positional emit. Compile one final time. A contract with a syntax mistake in one of these functions would fail to compile right here, on just this piece, rather than mixed in with a whole file's worth of errors if you'd written everything at once and compiled only at the end.

Compare, don't copy#

Open ../project/contracts/Greeter.vy. Function for function, it's the contract you just typed, just living in a file named Greeter.vy instead of GreeterPractice.vy (again: that filename, not an in-file name, is the only thing that distinguishes them to Ape). If your version differs slightly (a different parameter name, a comment you added), that's fine; what mattered was typing and compiling each piece separately, catching a mistake at the step that introduced it instead of at the end. You can delete GreeterPractice.vy now, or leave it; it doesn't get deployed by anything in this project unless you write a script that specifically does.