All articles
Web3 FoundationsSeptember 14, 20267 min read

public, private, internal, external: Solidity Visibility

The four Solidity visibility keywords, what each one actually restricts, why public on a variable never means writable, and why private does not mean secret on chain.

By Carlos (Bloqarl)

TL;DR

  • Solidity has four visibility keywords: public, external, internal, and private. They control who may call a function, not what it may do.
  • private does not mean secret. It restricts access from other Solidity code. All chain state is readable by anyone querying the chain directly.
  • On a state variable, public generates a getter so the value can be read from outside. It never grants write access.
  • external means callable only from outside the contract. internal means only from inside this contract and contracts that inherit from it.
  • Every function has a visibility whether or not it is written, so the keyword you see is a deliberate choice worth reading.

What are the visibility keywords in Solidity?

Solidity has four: public and external allow calls from outside the contract, while internal and private restrict calls to code inside the contract or its inheritance chain. Visibility answers exactly one question: who is allowed to call this.

Our worked contract uses two of them:

contract PiggyBank {
    address public owner;
    uint256 public goal;

    receive() external payable {}

    function progress() external view returns (uint256) {
        return address(this).balance;
    }

    function smash() external {

Two public state variables, three external functions. Everything in this contract is reachable from outside, which is a deliberate design: a glass pig, with its rules on the front, and every number readable by anyone.

What does each keyword restrict?

KeywordCallable from outsideCallable from inside this contractCallable from an inheriting contract
publicYesYesYes
externalYesNo, not directlyNo, not directly
internalNoYesYes
privateNoYesNo

Two rows deserve a second look.

external cannot be called from inside its own contract by the plain name. If smash() wanted to call progress(), it could not simply write progress(), because that function is external. This is why you occasionally see a contract split logic into an internal helper with a thin external wrapper around it: the helper is the part other functions can reach.

private is not inherited. A contract that inherits from another cannot call its parent's private functions. internal is the keyword for "this contract and its descendants", and it is what most shared helper code uses.

Why does public on a state variable not mean writable?

This is the single most common misreading of Solidity visibility, so it is worth being blunt.

    address public owner;

public here does one thing: it makes the compiler generate a small read-only function that returns the value. That generated function is called a getter, and it is why you can query owner() on a deployed contract and get an address back.

Nothing about public allows anyone to change owner. A state variable can only be written by code inside the contract that declares it. If no function in the contract assigns to it, its value is fixed forever, regardless of visibility. In PiggyBank, the only assignment to owner is in the constructor, so the owner is permanent. See the Solidity constructor and state variables in Solidity.

The mental split that fixes this for good: visibility controls reading and calling. Writing is controlled by whether any code exists to do it. They are separate questions and the keyword only answers the first.

Is a private variable actually private?

No, and this matters enough to say clearly.

Marking something private restricts access from other Solidity code. It does not encrypt anything, does not hide anything from users, and does not remove anything from the chain. Every value a contract stores is written into public chain state, and anyone can read that state directly, without the contract's cooperation and without any function being exposed.

So a private variable is private from other contracts. It is fully visible to any person who wants to look.

The practical consequence is straightforward: a contract cannot keep a secret. Any design that depends on a stored value being unknown, a password, an unrevealed number, a hidden threshold, is not achieving what it appears to. That is a design observation rather than a security lesson, and the security treatment is why crypto gets hacked.

What happens if you leave visibility out?

For functions, you cannot. Solidity requires an explicit visibility on every function, and omitting it is a compile error. That is a deliberate language decision: the default was previously public, which meant a forgotten keyword silently exposed a function to the world, so the language stopped allowing the omission.

For state variables you can omit it, and the default is internal. So this:

    uint256 goal;

is readable by the contract and its descendants, and has no automatically generated getter. The value is still in public chain state and still readable by anyone querying directly. The only thing missing is the convenient way to ask for it.

For a reader, that means an explicit public on a state variable is a choice someone made to expose a value through the contract's own interface. It is worth noticing which values a contract volunteers and which it does not.

How do you read visibility quickly?

Scan the signatures before the bodies, and sort into two piles.

The external and public functions are the contract's surface area: everything the outside world can trigger. That list, plus the state variables marked public, is the complete interface. Anything not on it cannot be called from outside, no matter what it does.

The internal and private functions are implementation. They exist to be used by the functions above, and you read them when you need to know how something works rather than what can be triggered.

Combine that with the state-mutability pass from view vs pure, inside the wider method in how to read a smart contract, and you can characterise an unfamiliar contract in two sweeps: which functions outsiders can call, and which of those can change anything. The intersection of those two sets is the part of the contract that actually matters, and in most real contracts it is short.

Delete the word and ask what breaks

  • Delete public from address public owner; and outsiders lose the generated getter. The contract still knows the owner, and anyone reading chain state directly still sees it.
  • Change external to internal on smash and nobody outside can ever call it. The ETH in the pig becomes unreachable, because the only function that pays it out can no longer be triggered.
  • Change external to public on progress and nothing visible changes for outside callers, but the function becomes callable from inside the contract too.
  • Mark a helper private in a contract designed to be inherited and every child contract loses access to it. This is the most common reason internal is chosen over private in library-style code.

Related questions

What is the difference between public and external in Solidity? Both allow calls from outside the contract. public additionally allows the function to be called from inside the contract by name, while external does not. For functions only ever called from outside, external is the more precise choice.

Is private data really private in a smart contract? No. private prevents access from other Solidity code, but all contract storage is publicly readable by querying the chain directly. Nothing stored on chain is hidden from people.

What is the default visibility in Solidity? Functions have no default and must declare one explicitly. State variables default to internal when no visibility is written.

Does public on a variable let anyone change it? No. It generates a read-only getter. A state variable can only be modified by code inside its own contract, so if no function assigns to it, the value cannot change at all.

What is the difference between internal and private? internal allows access from the contract itself and any contract inheriting from it. private allows access only from the contract where it is declared, not from children.

Can I call an external function from inside the same contract? Not by its plain name. You can call it through the contract's own address, which is a full external call with its own cost, but the usual pattern is to move shared logic into an internal function that both can use.

Where to go next

Visibility is a small vocabulary with one large trap in it: public is about reading, never writing, and private is about Solidity, never secrecy. Get those two straight and the rest is a four-row table.

The last word in our contract is the one that decides what happens when a rule is not met, and it behaves less like an error and more like an undo: require and revert in Solidity.

Tagged

SoliditySmart ContractsLearn to Code