When to use `external` instead of `public`

I'm trying to figure out when I'd want to change my function visibility from public to external.

When my function is marked public, I can call it from anywhere, and don't have to use this when calling it internally.

contract Get {
    function get() public {

    }
    function callGet() public {
        get();
    }
}

Are there other differences between public and external? If I'm writing a function that is going to be used by both contract and non-contract accounts and may or may not be extended, is there a nuance that could help me decide when to use external?

1 Like

There is no difference as far as other contracts are concerned. Internally, you will want to use internal calls (without this) 99% of the time, so I would recommend to just stick with public.

Public and External are both callable from or outside smart contract, where in the internal function is only callable only with in the smart contract.

The only other difference between public and external is the gas cost--external is slightly cheaper than public. However, the gas savings are marginal at best (I think it's 20 units, I could be wrong), so external isn't really worth using over public unless you're doing so for organizational purposes (i.e. to remind yourself and other devs to NOT call that function internally).

This isn't really true as far as I know. Can you provide an example that shows this cost difference?

Ah, I appear to be wrong... mostly :wink:

I just did some split testing and the results are somewhere between inconclusive and insignificant. public beats external in gas costs when used with empty functions, but external beats public for view functions with no input parameters. However, public always beats external when input parameters are involved.

public wins in most contexts, external wins when getting values from state variables without input parameters, and in either case the gas difference is marginal--exactly 22 gas when tested in isolation.

When there is only one function in the contract, public and external view functions with no inputs cost the same (122 gas). I figured maybe it has to do with function quantity and ordering, so I reversed their ordering in test 4a, but it produced the same result as test 4--external beat public by 22 gas no matter which order they appear in. So, function quantity mattered, but ordering did not.

The only time when external soundly beats public is in test 1 by 44 gas, and I think it's due to the quantity and ordering of other functions in the contract since it doesn't replicate that cost difference when those two functions are isolated.

Other than a return, none of my tests involve internal logic, and it probably doesn't matter. I tested a few pure functions, but they seem to have the same results as view functions, where external beats public when there are no input parameters.

These were the tests I did (Remix, compiler 0.8.17):

contract ExternalPublicTester1 {
    // Several view and non-view in same contract
    // Results: Public beats external most of the time, function ordering and quantity costs
    // interfere with gas difference measurements
    // 209
    function externalTest() external {}
    // 445
    function externalTest2(uint256 someUint) external {}
    // 166
    function externalViewTest3() external view {}
    // 423
    function externalViewTest3(uint256 someUint) external view {}
    // 144
    function publicTest() public {}
    // 446
    function publicTest2(uint256 someUint) public {}
    // 210
    function publicViewTest3() public view {}
    // 401
    function publicViewTest3(uint256 someUint) public view {}
}

contract ExternalPublicTest2 {
    // Non-view, 2 params, no returns
    // Result: Public beats external by 22 gas
    // 572
    function externalTest(uint256 someUint, uint256 anotherUint) external {}
    // 550
    function publicTest(uint256 someUint, uint256 anotherUint) public {}
}

contract ExternalPublicTest3 {
    // View, no params, no return values
    // Result: External beats public by 22 gas
    // 122
    function externalViewTest3() external view {}
    // 144
    function publicViewTest3() public view {}
}

// Isolated view functions
// Result: Same gas costs for external and public
contract ExternalPublicTest3a {
    // 122
    function externalViewTest3() external view {}
}

contract ExternalPublicTest3b {
    // 122
    function publicViewTest3() public view {}
}

contract ExternalPublicTest4 {
    // View, no params, 1 return value
    // Result: External beats public by 22 gas
    uint256 someUint;
    // 2415
    function externalViewTest3() external view returns(uint) {return someUint;}
    // 2437
    function publicViewTest3() public view returns(uint) {return someUint;}
}

contract ExternalPublicTest4a {
    // Same as 4, function ordering reversed
    // Result: External beats public by 22 gas
    uint256 someUint;
    // 2437
    function publicViewTest3() public view returns(uint) {return someUint;}
    // 2415
    function externalViewTest3() external view returns(uint) {return someUint;}
}

// View, no inputs, 1 return, isolated
// Result: Same gas costs
contract ExternalPublicTest4b {
    uint256 someUint;
    // 2415
    function externalViewTest3() external view returns(uint) {return someUint;}
}

contract ExternalPublicTest4c {
    uint256 someUint;
    // 2415
    function publicViewTest3() public view returns(uint) {return someUint;}
}


contract ExternalPublicTest4d {
    // Pure, no params, 1 return
    // Result: External beats public by 22 gas
    uint256 someUint;
    // 307
    function externalViewTest3() external pure returns(uint) {}
    // 329
    function publicViewTest3() public pure returns(uint) {}
}

contract ExternalPublicTest5 {
    // View, 1 param, 1 return
    // Result: Public beats external by 22 gas
    uint256 someUint;
    // 2702
    function externalViewTest3(uint256 inputUint) external view returns(uint) {return someUint;}
    // 2680
    function publicViewTest3(uint256 inputUint) public view returns(uint) {return someUint;}
}

There are gas cost differences between them, but they aren't consistent, they are heavily dependent on context, and they are overwhelmed by the presence and ordering of other functions in the contract. Then, add any amount of logic in and the difference doesn't matter at all.

My conclusion is that external is cheaper than public in certain situations, while public is cheaper than external in all the rest--but the difference is only around 22 gas.

You need to test one function per contract. Otherwise the costs are affected by the function dispatch. When the contract receives a call it has to dispatch it to one of the functions and this is different for different functions, for reasons that are unrelated to public vs external.

This is the real result.

If you are not going to use the function inside smart contract, or more specifically, if you are not going to use a public function inside any other function in the same contract, you can mark visibility as external.

Function with external visibility marked means that function can only be called from outside the contract or by other contracts.

Please remember, public functions cause more gas cost than external one.