Why Vyper’s Immutable Variables Save Gas – A Practical Guide
Immutable variables let Vyper contracts embed constants in bytecode, cutting read costs and storage use. This post explains how they work, shows a real example, highlights trade‑offs, and tells you how to verify the savings in your own project.
28 Feb 2026, 02:11 UTC

Concrete Problem: Why Read Gas Matters
When a contract stores a value in regular storage, every read costs a full SLOAD operation (≈800 gas on Ethereum). If that value never changes – an owner address, a protocol fee, or a fixed token name – you can avoid that cost by embedding the value directly into the contract’s bytecode. Vyper’s immutable keyword lets you do exactly that.
Thesis: Immutable Variables Are Like Constants, but Deployed
In Vyper, an immutable variable is declared with the keyword, assigned once in the constructor, and thereafter behaves like a constant. Because the value is baked into the bytecode, every read is just a memory lookup, not a storage read. This saves gas, reduces storage usage, and gives you a compile‑time guarantee that the value cannot be changed.
How Immutables Work in Vyper
Declaration and Assignment
Use immutable in the variable type. The assignment must appear exactly once in the constructor, and the variable cannot be reassigned elsewhere.
@external
def __init__(owner: address):
self.owner: immutable(address) = owner
Bytecode Embedding
During compilation, the compiler replaces every reference to self.owner with the literal value. The bytecode contains the value in the CODECOPY section, so no storage slot is allocated. The contract’s storage layout remains unchanged, but the read cost drops from an SLOAD to a simple CALLDATALOAD‑style fetch.
Worked Example: Owner‑Only Contract
Below is a minimal contract that stores an owner address immutably and exposes a view function to read it.
# SPDX-License-Identifier: MIT
# pragma: vyper 0.3.3
@external
def __init__(owner: address):
self.owner: immutable(address) = owner
@view
@external
def get_owner() -> address:
return self.owner
Compile with vyper --version to ensure you are on 0.3.0 or newer. Deploy the contract on a local testnet (Hardhat or Foundry) and call get_owner() – you should see the address you passed in the constructor.
Verifying Gas Savings
- Compile with
vyper --gas-reportand note the cost of callingget_owner()– it should be around 200 gas, far less than a storage read. - Replace
immutable(address)with a normal state variableowner: public(address)and re‑compile. The same call now costs ≈800 gas. - Deploy both versions and compare the total gas used for a transaction that reads the owner twice.
Trade‑Offs and Limitations
- No Updates After Deployment – Once set in the constructor, the value cannot change. If you need a mutable owner, use a regular state variable.
- Upgradeability Constraints – Upgradeable proxy patterns usually require that the storage layout remain stable. Because immutable values are stored in bytecode, a proxy cannot change them, limiting certain upgrade scenarios.
- Constructor Knowledge – You must know the value at deployment time. Dynamic values that depend on external calls during construction cannot be made immutable.
- Version Requirement – The keyword is only available from Vyper 0.3.0 onward. Older compilers will reject the syntax.
Actionable Checklist for Your Project
- Identify constants that never change (owner, fee, token name).
- Declare them with
immutableand assign in the constructor. - Run
vyper --gas-reportto confirm the reduced read cost. - Ensure your deployment workflow passes the value at construction time.
- Review upgrade patterns – if you plan to use a proxy, consider whether the immutable value can stay constant.
By following these steps, you can reduce gas consumption, tighten security guarantees, and keep your contracts lean.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.