BlockchainSolidity9 min readUpdated

Functions and Visibility in Solidity: public, external, internal, private

By Mudassir Khan — Agentic AI Consultant & AI Systems Architect, Islamabad, Pakistan

Cover illustration for: Functions and Visibility in Solidity: public, external, internal, private

Section 01 · Why this matters

Function visibility is the security perimeter

The wrong visibility keyword turns an internal helper into a public attack surface. Most beginner exploits trace back to one missing keyword.

Quick answer

Why does visibility matter? Because in Solidity 0.5 and later, every function must declare its visibility explicitly. There is no default. The keyword controls who can invoke the function — only the contract itself, only inherited children, only external callers, or anyone in the world. Picking the wrong level creates real exploits: a 'helper' that turns out to be public, a 'private' function that is accessible to inheriting contracts, or an external function that is callable internally with the wrong gas profile.

The four levels do not just decide who can call. They also decide whether the function shows up in the contract's ABI (its public interface), how arguments are passed (memory vs calldata), and what optimisations the compiler can apply.

Section 02 · The four keywords

public, external, internal, private

Walk through each one with a concrete example and the rule of thumb for picking it.

public

Callable from inside the contract, from inherited contracts, and from the outside world. Public state variables get an automatic getter function. Pick public when you need both internal and external access. State variables should be public only when an external getter is genuinely part of the interface — otherwise default to internal.

external

Callable only from outside the contract. Other contracts and externally owned accounts can call it; the contract itself cannot, except through the address(this).call pattern. external is slightly cheaper than public when arguments are large arrays or strings, because the data stays in calldata instead of being copied into memory.

internal

Callable from inside the contract and from any contract that inherits from it. The natural choice for helper logic that is shared across child contracts but must never be exposed to the outside world. State variables default to internal if you do not specify a visibility.

private

Callable only from inside the exact contract that declares it. Inheriting contracts cannot call it. Important caveat: private restricts who can call the function, not who can read the data. All contract storage is publicly readable on chain regardless of the keyword. Use private for code organisation, not for hiding secrets.

Section 03 · State mutability

view, pure, payable — the second axis

Visibility says who can call. Mutability says what the function is allowed to touch. The combination is what fully describes a function.

Table of state mutability keywords pure view default and payable showing what each can do.
Pick the most restrictive mutability that still works. The compiler will warn you if you ask for more freedom than you actually use.
solidity
function add(uint256 a, uint256 b)
    external
    pure
    returns (uint256)
{
    return a + b;       // does not read state, does not write state
}

function balanceOf(address user)
    external
    view
    returns (uint256)
{
    return balances[user];   // reads state, does not write
}

function deposit()
    external
    payable
{
    balances[msg.sender] += msg.value;   // writes state, accepts ether
}

Two practical rules. First, view and pure functions can be called for free off chain through eth_call — no gas, no transaction. That makes them the right shape for any read used by a frontend. Second, payable is the only way to receive ether. Forget it on a deposit function and every transfer reverts.

Section 04 · Anatomy

The full function signature

A complete function declaration in canonical order — name, parameters, visibility, mutability, modifiers, returns.

solidity
function transfer(address to, uint256 amount)        // name + parameters
    public                                                // visibility
    payable                                               // mutability
    onlyWhitelisted                                       // modifier
    returns (bool success)                                // return type
{
    require(balances[msg.sender] >= amount, "insufficient");
    balances[msg.sender] -= amount;
    balances[to]         += amount;
    emit Transfer(msg.sender, to, amount);
    return true;
}

The order is fixed. Visibility comes first, then mutability, then any modifiers, then returns. Inside the body, you have access to msg.sender (the immediate caller's address), msg.value (incoming ether, only meaningful in payable functions), and block.timestamp (the current block's timestamp).

Default to most restrictive

A safe habit: write every new function as private, then loosen to internal, external, or public only when a caller actually needs the access. The compiler will surface every place that breaks. Going from private outward keeps the attack surface minimal.

Section 05 · Patterns

Visibility patterns from production code

solidity
// 1. Public state variable — automatic getter
mapping(address => uint256) public balances;
// generates: function balances(address) external view returns (uint256)

// 2. External entry point + internal implementation
function deposit() external payable {
    _deposit(msg.sender, msg.value);
}

function _deposit(address user, uint256 amount) internal {
    balances[user] += amount;
    emit Deposit(user, amount);
}

// Children that inherit can call _deposit, the outside world only sees deposit.

// 3. View function for off-chain reads
function totalSupply() external view returns (uint256) {
    return _totalSupply;
}

// 4. Pure helper for math
function squareRoot(uint256 x) internal pure returns (uint256) {
    if (x == 0) return 0;
    uint256 z = (x + 1) / 2;
    uint256 y = x;
    while (z < y) { y = z; z = (x / z + z) / 2; }
    return y;
}

Section 07 · FAQ

Frequently asked questions

What is the difference between public and external?

Both are callable from outside. The difference is internal accessibility: public can be called from inside the contract too, external cannot (unless you use this.functionName(), which routes through the external interface and pays the cost). external is slightly cheaper for large array arguments because they stay in calldata.

Can private data be read on chain?

Yes. private and internal restrict code, not state. Anyone running an Ethereum node can read every byte of any contract's storage with eth_getStorageAt. If you store a password, an API key, or any unhashed secret, treat it as public. Sensitive data either stays off chain or is hashed first.

What does view do exactly?

view promises the function will not modify state. The compiler enforces this — assigning to a state variable in a view function is a compile error. view functions can be called for free off chain through eth_call, which is why frontends use them for reads.

What is the difference between view and pure?

view reads state but does not write. pure neither reads nor writes — it cannot even reference state variables, msg.sender, or block.timestamp. pure is the right keyword for math helpers, hashing utilities, and anything that depends only on its arguments.

Why must a deposit function be payable?

Solidity rejects any incoming ether unless the receiving function is marked payable. This is a safety feature — without it you could accidentally accept ether into a function that has no logic to track the deposit. The same applies to the receive() and fallback() functions; both must be payable to accept plain ether transfers.

Written by Mudassir Khan

Agentic AI consultant and AI systems architect based in Islamabad, Pakistan. CEO of Cube A Cloud. 38+ agentic AI launches delivered for global founders and CTOs.

View agentic AI consulting serviceSee ChainTrust case study

Related service

Agentic AI Consulting

See scope & pricing →

Related case study

ChainTrust Compliance Engine

Read case study →

More on this topic

Need an AI systems architect?

Book a 30-minute architecture call. I will sketch the high-level design for your use case and give you an honest view of the trade-offs.

Book a strategy call →