akash11

Visibility in Solidity

A comprehensive guide to visibility modifiers in Solidity, covering how public, private, internal, and external keywords control access to state variables and functions within contracts and their inheritance chains.

Published on 04 November, 2025

Categories:

Solidity

Understanding Visibility Modifiers

Solidity provides four visibility modifiers that determine how and where state variables and functions can be accessed. Think of these as security gates that control who gets to see and use different parts of your smart contract code.
The four visibility types are:
  • public: accessible everywhere
  • private: accessible only within the current contract
  • internal: accessible within the current contract and derived contracts
  • external: accessible only from outside the contract (functions only)

State Variable Visibility

State variables can use three visibility modifiers: public, private, and internal. External is not allowed for state variables.

Public State Variables

When you declare a state variable as public, Solidity automatically creates a getter function for it. This means anyone can read the value from outside the contract.
contract VisibilityExample { uint256 public myNumber = 42; // Solidity creates: function myNumber() public view returns (uint256) }
In this example, any external contract or user can call myNumber() to get the value 42. You don't write the getter yourself, Solidity does it for you.

Private State Variables

Private variables can only be accessed by functions inside the same contract. Even contracts that inherit from this contract cannot access private variables.
contract MyContract { uint256 private secretValue = 100; function revealSecret() public view returns (uint256) { return secretValue; // This works } } contract ChildContract is MyContract { function tryToAccessSecret() public view returns (uint256) { // return secretValue; // ERROR: Cannot access private variable } }
Important note: private doesn't mean the data is hidden from the blockchain. Anyone can still read the storage slots directly. Private only controls access at the Solidity code level.

Internal State Variables

Internal is the default visibility for state variables if you don't specify one. Internal variables can be accessed within the contract and any contract that inherits from it.
contract Parent { uint256 internal familySecret = 500; } contract Child is Parent { function accessFamilySecret() public view returns (uint256) { return familySecret; // This works } }

Function Visibility

Functions can use all four visibility modifiers. Choosing the right one affects gas costs and security.

Public Functions

Public functions can be called internally (within the contract) or externally (from outside). They're the most flexible but also the most expensive in terms of gas when called internally.
contract Calculator { function add(uint256 a, uint256 b) public pure returns (uint256) { return a + b; } function calculate() public pure returns (uint256) { return add(10, 20); // Internal call to public function } }

External Functions

External functions can only be called from outside the contract. They cannot be called internally using this.functionName() or just functionName(). To call them internally, you must use this.functionName(), which is expensive.
contract DataProcessor { function processData(uint256[] calldata data) external pure returns (uint256) { return data.length; } function internalProcess() public pure returns (uint256) { uint256[] memory testData = new uint256[](3); // return processData(testData); // ERROR: Cannot call directly return this.processData(testData); // Works but expensive } }
External functions are cheaper for large arrays because they can use calldata instead of copying data to memory.

Internal Functions

Internal functions can be called within the contract and by derived contracts. They're commonly used as helper functions.
contract Math { function multiply(uint256 a, uint256 b) internal pure returns (uint256) { return a * b; } function square(uint256 x) public pure returns (uint256) { return multiply(x, x); // Works fine } } contract AdvancedMath is Math { function cube(uint256 x) public pure returns (uint256) { return multiply(x, multiply(x, x)); // Inherited function accessible } }

Private Functions

Private functions work exactly like internal functions except they cannot be accessed by derived contracts. Use them when you want to keep implementation details truly private to one contract.
contract Secure { uint256 private counter; function incrementCounter() private { counter++; } function doSomething() public { incrementCounter(); // Works } } contract Extended is Secure { function tryIncrement() public { // incrementCounter(); // ERROR: Cannot access } }

Visibility in Inherited Contracts

When working with contract inheritance, understanding visibility becomes critical. Here's how it plays out across the inheritance chain:
contract Base { uint256 public publicVar = 1; uint256 internal internalVar = 2; uint256 private privateVar = 3; function publicFunc() public pure returns (string memory) { return "public"; } function internalFunc() internal pure returns (string memory) { return "internal"; } function privateFunc() private pure returns (string memory) { return "private"; } } contract Derived is Base { function testAccess() public view returns (uint256) { // Can access public and internal from parent uint256 sum = publicVar + internalVar; // uint256 cant = privateVar; // ERROR publicFunc(); // Works internalFunc(); // Works // privateFunc(); // ERROR return sum; } }
Child contracts inherit public and internal members but never private ones. This creates a clear boundary for what's truly encapsulated versus what's shared within a contract family.

Practical Guidelines

When choosing visibility, follow these rules:
  1. Default to the most restrictive visibility that works. Start with private, then internal, then public or external as needed.
  2. Use external for functions that are only called from outside, especially when they accept large arrays. This saves gas.
  3. Use internal for helper functions that child contracts might need. This is cleaner than making everything public.
  4. Remember that private state variables are still visible on the blockchain. For actual secrets, use encryption off-chain.
  5. Public state variables automatically get getters. Don't write your own unless you need custom logic.

Common Mistakes

contract Mistakes { // Mistake 1: Making everything public when it doesn't need to be uint256 public helperValue; // Should be internal or private // Mistake 2: Using public when external would work function processArray(uint256[] memory data) public pure returns (uint256) { // Should be external with calldata return data.length; } // Mistake 3: Forgetting visibility defaults to internal for functions function oops() pure returns (uint256) { // Not callable externally! return 42; } }

Visibility and Security

Visibility is your first line of defense in smart contract security. Exposing functions or variables unnecessarily increases your attack surface. Each public or external function is a potential entry point for malicious actors.
contract SecureExample { mapping(address => uint256) private balances; function deposit() external payable { balances[msg.sender] += msg.value; } // Good: Helper function not exposed function updateBalance(address user, uint256 amount) private { balances[user] = amount; } // Good: Only expose what's necessary function getBalance() external view returns (uint256) { return balances[msg.sender]; } }
Visibility modifiers in Solidity are fundamental to building secure and efficient smart contracts. They control access at the code level, manage gas costs, and define clear boundaries in contract inheritance. Master these concepts and you'll write cleaner, safer Solidity code that properly encapsulates logic while exposing only what's necessary to the outside world.

Copyright © 2025

Akash Vaghela