Solidity Syntax, Types, and Operators
The concept
Solidity compiles to EVM bytecode, so every type you declare has a direct storage and gas cost — unlike Python or JavaScript, where types are mostly a runtime convenience. A Solidity file starts with a pragma line (pragma solidity ^0.8.24;) that pins the compiler version range, because breaking changes between major versions are common and the compiler refuses to build outside the declared range. After the pragma comes an optional license identifier (// SPDX-License-Identifier: MIT), then one or more contract declarations — a contract is both a class definition and a deployable unit. Unlike a class in an object-oriented language, a contract also has its own address, its own balance, and its own persistent storage once deployed; there's no equivalent of an "instance" you create in memory and discard — every contract you deploy is a standalone account on the blockchain.
Value types are copied whenever they're assigned or passed: uint256 and int256 (and their smaller siblings uint8...uint256 in steps of 8, used for packing), bool, address (a 20-byte Ethereum account identifier) and address payable (the same, but allowed to receive Ether via .transfer() or .call{value: x}()), and fixed-size byte arrays like bytes32. Reference types — arrays, mapping, and struct — are not copied by default; they live in storage, memory, or calldata depending on where they're declared, and assigning one reference type to another usually copies a pointer, not the data (storage-to-storage struct assignment is the one exception that deep-copies). Knowing which category a type falls into matters the moment you pass it into a function: pass a uint256 and you get an independent copy inside the callee; pass a storage array reference and the callee is mutating the exact same slots as the caller.
Solidity shares + - / % * == != < > <= >= && || ! & | ^ ~ << >> with C-family languages, but adds unit suffixes: 1 ether is shorthand for 1e18 wei, and 1 gwei for 1e9 wei, because Ether amounts are always tracked in wei (the smallest indivisible unit) on-chain — there's no floating-point type in Solidity, so "1.5 ETH" in a UI is really 1_500_000_000_000_000_000 wei under the hood, and you should think in integers from day one. Since Solidity 0.8, arithmetic overflow and underflow revert automatically — you no longer need the community's old SafeMath library for the common case, though you can still opt into unchecked { ... } blocks for gas savings when you've already proven the bounds yourself (a loop counter that can never exceed array.length, for example). Strings and dynamic bytes are reference types too, and string concatenation isn't an operator — you reach for string.concat(a, b) or abi.encodePacked instead, another small but common surprise for developers coming from higher-level languages.
Worked example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Counter {
uint256 public count;
function increment() external {
count += 1;
}
}
Paste this into Remix (remix.ethereum.org) or a local Foundry project, compile with the Solidity compiler matching the pragma, and deploy to Remix's JavaScript VM. Calling increment() costs roughly 43,000 gas the first time (a cold SSTORE from zero to non-zero) and about 23,000 gas on repeat calls (a warm slot that's already non-zero) — you can see this difference directly in Remix's transaction log, and it's a preview of the storage-cost lesson that follows this one. The public keyword on count auto-generates a getter function, so count() is callable externally with no code of your own — the compiler writes that function for you based purely on the variable's visibility.
Try it
Extend Counter with:
- A
decrement()function that reverts automatically on underflow (Solidity 0.8+ does this for you — call it whencount == 0and confirm the revert in Remix's console). - A
mapping(address => uint256) public callCounts;that incrementscallCounts[msg.sender]every timeincrement()runs. - A constant
uint256 public constant MAX_COUNT = 1000;and arequireinincrement()that reverts oncecountreaches it — note thatconstantvalues are substituted at compile time and cost no storage slot at all, unlike a regular state variable.
Deploy to the JavaScript VM, call increment() from two different test accounts (Remix lets you switch the "Account" dropdown), then read back callCounts for each address to confirm they're tracked independently. Note the gas cost difference between the first write to a mapping slot (cold) and the second (warm), and compare it against the cost of reading MAX_COUNT, which should show as free regardless of how many times you call it.
Recap
- Types carry real gas cost in Solidity — there's no "just a variable" the way there is in Python, and every declaration is a decision about storage, memory, or calldata.
- Value types copy; reference types (arrays, mappings, structs) point at storage/memory/calldata locations, so passing them around can alias the original data.
- Solidity 0.8+ reverts on overflow/underflow by default, removing the need for manual SafeMath checks, while
constant/immutablevalues avoid storage costs entirely.
Resources
- Solidity docs: Types
- Solidity docs: Units and Globally Available Variables
- Remix IDE — browser sandbox used in this lesson's exercises
- Solidity by Example: Variables