Vyper Immutable vs Constant: Choosing Where a Fixed Value Lives
Decide between Vyper immutable and constant by asking where the value comes from. Compare storage, read gas and flexibility, with an example and verification steps.
06 Jul 2026, 08:21 UTC

You have a value that must not change after deployment, but you are not sure whether it belongs in the contract bytecode or in a storage slot. In Vyper 0.3 and later that choice is constant versus immutable. Choosing wrong either burns a storage read on every call or forces you to rebuild and redeploy the contract for each environment.
The decision and its constraints
Pick constant when the value is known while you are writing the source: a fee in basis points, a version tag, a hard-coded address on one network. Pick immutable when the value arrives at deployment time as a constructor argument or from deployment tooling, and must stay fixed for the life of the contract.
constantrequires a literal at the declaration. It cannot be assigned in__init__.immutablemust be assigned exactly once in__init__and cannot be written afterwards.- Both are read-only after initialization, so neither gives you a setter.
- Version matters. In Vyper 0.2.x,
constanthad different runtime-initialization semantics. The behavior described here assumes Vyper 0.3.0 or newer; checkvyper --versionbefore relying on it.
Comparing the two options
| Property | immutable | constant |
|---|---|---|
| Where the value lives | Contract storage slot | Inlined into bytecode |
| How it is set | Once, in __init__ | At compile time, in source |
| Deployment cost | Constructor executes a storage write | No initialization work at runtime |
| Read cost | Storage read (SLOAD), commonly a few hundred gas | Near zero, value pushed onto the stack |
| Per-deployment customization | Yes, via constructor arguments | No, requires a new build |
| Typical use | Owner, treasury, router address, chain-specific config | Protocol constants, version strings, fixed ratios |
Trade-offs in practice
immutable buys flexibility. The same compiled artifact can be deployed to several networks with different constructor arguments, which keeps your build reproducible and your deployment scripts simple. The cost is one storage slot and a storage read every time the value is used. On a function that reads the value once, that is negligible. In a loop or a hot path called thousands of times per block, it is not.
constant buys cheap reads and a smaller runtime footprint, because the value is a literal in the bytecode rather than a slot that must be loaded. The cost is rigidity: changing the value means editing source, recompiling, re-verifying and redeploying. If your value differs per chain, you end up maintaining one source variant per chain, which is exactly the kind of divergence that causes audit findings.
A common pattern is to use both in the same contract: constant for values that are genuinely universal, immutable for values that are deployment-specific.
A concrete example
The following contract, written against Vyper 0.3.x, declares one of each. It is illustrative source, not a tested deployment artifact.
# Vyper 0.3.x
immutable public owner: address
constant public FEE_BPS: uint256 = 10
@external
def __init__(_owner: address):
self.owner = _owner
@external
def get_fee() -> uint256:
return self.FEE_BPS
owner is supplied by whoever deploys the contract and is written into a storage slot during __init__. FEE_BPS never touches storage; every read compiles to pushing the literal 10.
Verifying which one you actually got
Run these from your project directory with Vyper installed and write access to the output path. No special permissions are needed beyond that.
- Confirm the compiler version:
vyper --version. If it reports 0.2.x, the semantics below do not apply. - Inspect the intermediate representation:
vyper contract.vy -f ir. Look for the immutable variable annotated as such with an assignment inside the constructor, and for the constant appearing as a literal value rather than a storage load. - Inspect the bytecode:
vyper contract.vy -f bytecode. The constructor should contain a storage write forowner, whileFEE_BPSshould appear as an inlined value with no corresponding storage access.
For a gas check, write a test that calls get_fee() and a getter for owner, recording gas used for each. The owner getter should cost measurably more because it performs a storage read. Do not assume a fixed number: optimizer settings, compiler version and surrounding code all shift the result, so measure in the exact build configuration you intend to deploy.
Limitations and how to check your choice
Neither option can be changed after deployment. If a value might need to change, use neither: a storage variable with an access-controlled setter is the correct tool, and the read cost is the price of that flexibility. Immutable values also add to deployment bytecode and constructor gas, which matters if you are near a size or gas limit.
A practical check before committing: list every fixed value in the contract and ask whether it differs between any two deployments you plan to make. If yes, it must be immutable or a settable storage variable. If no, make it constant and remove a storage read from every call path that touches it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.