Using Vyper Immutable Variables to Save Gas on Configuration Values
Learn how Vyper’s immutable variables embed configuration values in creation code to avoid costly SLOAD reads, with a worked token example and deployment verification steps.
27 Sept 2026, 17:00 UTC

Problem: Configuration values cost extra gas to read
When a contract needs a fixed value set at deployment—such as a token’s total supply, an admin address, or a fee basis points—developers often store it in a regular state variable. Reading that variable at runtime requires an SLOAD opcode, which costs 2100 gas (or more with warm storage). If the value never changes after deployment, that cost is unnecessary.
Thesis: Vyper’s immutable keyword embeds the value in creation code, eliminating SLOAD for reads
By declaring a variable with immutable, you assign it exactly once during the constructor. The Vyper compiler places the value directly in the contract’s creation bytecode. Subsequent reads are pure code accesses, costing only the gas of a PUSH operation (≈3 gas). This pattern gives you deploy‑time configurability without the runtime storage overhead.
How immutables work in Vyper
An immutable variable behaves like a constant that is set from constructor arguments. The compiler treats it as a value type (e.g., uint256, bool, address, bytes32, or fixed‑size bytes[N]) and stores the supplied argument in the contract’s creation code. After deployment, the variable cannot be reassigned; any attempt to write to it results in a compile‑time error.
Declaration syntax
# vyper >=0.2.0
public immutable total_supply: uint256
@external
def __init__(_total_supply: uint256):
self.total_supply = _total_supply
@external
def get_supply() -> uint256:
return self.total_supply
The public annotation automatically generates a getter function (get_supply in this case) that returns the immutable’s value.
Worked example: ERC‑20‑like token with immutable supply
We’ll create a minimal token contract where the total supply is set at deployment and never changes. The example shows the code, compilation command, and how to verify that the value is embedded in the creation bytecode.
1. Write the contract (Token.vy)
# Token.vy
# vyper >=0.2.0
public immutable name: String[32]
public immutable symbol: String[10]
public immutable decimals: uint8
public immutable total_supply: uint256
balances: HashMap[address, uint256]
@external
def __init__(
_name: String[32],
_symbol: String[10],
_decimals: uint8,
_total_supply: uint256
):
self.name = _name
self.symbol = _symbol
self.decimals = _decimals
self.total_supply = _total_supply
balances[msg.sender] = _total_supply
@external
def transfer(_to: address, _amount: uint256):
assert balances[msg.sender] >= _amount, "Insufficient balance"
balances[msg.sender] -= _amount
balances[_to] += _amount
@external
def balance_of(_account: address) -> uint256:
return balances[_account]
2. Compile and inspect creation bytecode
Run the following in a terminal where vyper (>=0.2.0) is available. No special permissions are needed.
vyper -f bytecode Token.vy > Token.bin
# Look for the embedded values near the end of the creation code
# Example: search for the hex representation of the supply
grep -o "$(printf '%064x' 1000000000000000000)" Token.bin || echo "value not found"
If you supplied 1000000000000000000 (1 ether with 18 decimals) as _total_supply, you should see that exact 32‑byte word appearing in the creation code. The presence of the value confirms it is baked in, not stored in contract storage.
3. Deploy with different constructor arguments and verify cheap reads
Using a local testnet (e.g., Foundry or Hardhat), deploy two instances:
forge create Token.vy --constructor-args "Token A" "TKA" 18 1000000000000000000 --rpc-url http://127.0.0.1:8545forge create Token.vy --constructor-args "Token B" "TKB" 6 500000000 --rpc-url http://127.0.0.1:8545
Then call the getter (total_supply) via cast call or a Foundry test. The transaction trace will show no SLOAD opcode for the getter; only PUSH operations to return the embedded constant.
Trade‑offs and limitations
While immutables save gas, they come with constraints:
- One‑time assignment: The value must be set in the constructor; changing it later requires a new contract deployment.
- Type restrictions: Only value types and fixed‑size byte arrays are allowed. Mappings, dynamic arrays, or structs cannot be immutable.
- Constructor‑argument safety: If any constructor argument is derived from an external call that could revert, the entire deployment transaction fails, leaving no contract deployed.
- Bytecode size: Each immutable adds its value to the creation code, increasing the contract’s deployment size. For very large numbers of immutables, deployment gas may rise.
Practical check: after deployment, run cast tx and verify that the getter’s internal calls contain no SLOAD opcodes. If you see an SLOAD, the variable was not declared immutable.
Actionable closing
If your contract needs a configuration value that never changes after launch, prefer Vyper’s immutable over a regular state variable. Follow these steps:
- Declare the variable with
immutableand an appropriate value type. - Assign it once in the
__init__function using a constructor argument. - Compile with
vyper >=0.2.0and inspect the creation bytecode to confirm the value is embedded. - Deploy with the desired arguments and verify that getters incur no
SLOADgas. - Remember that any change requires a new contract deployment; plan your upgrade strategy accordingly.
By using immutables, you keep deployment flexibility while eliminating unnecessary storage reads, leading to lower transaction costs for users and a cleaner gas profile.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.