Symbols, scope and binding
A symbol is a name the program gives to something, and scope is the set of rules that decides which definition a given use of that name actually refers to.
A symbol is a name the program gives to something: a function, a variable, a type, a class. A scope is the region of the code where a given name is valid, and the rules of scope decide, for any single use of a name, which definition that use actually points at. Getting from a name to the exact thing it refers to is called binding, and it is the reason plain text search over a name is wrong so often.
A name is not the thing
Start with the most basic confusion, because everything else follows from clearing it up. The word total written in your code is not the thing it refers to. It is a label pointing at a thing. The thing is a function somewhere, or a variable holding a number, or a field on an object. The label and the thing are separate, and the same label can point at completely different things in different places.
This is not a quirk of programming. It is how names work everywhere. The word bank means the side of a river in one sentence and a place that holds money in another, and you do not even notice the switch because the surrounding context tells you which is meant. Someone says John at a family dinner and means one person; someone says John at your office and means another. The name is shared. The meaning is decided by where you are standing when you hear it. Code is the same, except the rules that decide the meaning are exact and written down, which is both good news and the source of the difficulty.
The industry word for one of these names is a symbol. When the code introduces a symbol, at a declaration, it is like adding a name to a directory. When the code later uses that symbol, the language has to look it up in the right directory to know what is meant. The rules that say which directory to look in, and in what order, are the rules of scope.
Scope, from the ground up
A scope is a region of code within which a name has a particular meaning. Scopes nest inside one another like rooms inside a building, and a name introduced in an inner room is usually visible in that room and any room inside it, but not out in the hallway. This nesting is why the same short name can be reused all over a large program without the uses colliding: each one lives in its own room.
It helps to name the common levels from the outside in.
- Global scope is the outermost room. A name defined here is visible everywhere. In practice programs keep this room nearly empty on purpose, because anything in it can be seen and disturbed from anywhere.
- Module or file scope is the set of names a single file defines for itself and, often, chooses to share with other files. Most of a program’s real structure lives at this level.
- Function scope is the room created when a function runs. Its parameters and the variables it declares live here and vanish when the function returns.
- Block scope is a smaller room still: the inside of a loop, an if branch, or any set of braces. A name declared here may not even outlive the loop it sits in.
The key rule is that a use of a name is resolved by searching outward, from the closest room to the farthest, and stopping at the first match. You look in the current block, then the enclosing function, then the file, then the global room. The nearest definition wins. That one rule, nearest wins, explains almost everything that follows.
A small example
Here is a short piece of code with the name rate appearing more than once, meaning something different each time.
rate = 0.05 # module scope
def quote(amount):
rate = 0.09 # function scope
return amount * rate
def audit(amount):
return amount * rateThere are two different things both called rate. Inside quote, the name rate resolves to the local one worth 0.09, because the nearest definition wins and the function has its own. Inside audit, there is no local rate, so the search moves outward and finds the module one worth 0.05. Same four letters, two distinct values, and the only way to know which is which is to know the scope rules and where each use is standing. A reader who treats the word rate as a single thing will get the wrong answer half the time.
Shadowing: the same name hiding another
What happened inside quote has a name. Shadowing is when a name introduced in an inner scope hides a name of the same spelling in an outer scope, for the length of that inner scope. The outer one is not gone. It is covered up, the way standing close to a small object can hide a larger one behind it. Step out of the inner room and the outer name is visible again.
Shadowing is deliberate and usually harmless: a loop counter called i inside one function has nothing to do with a loop counter called i inside another, and shadowing is what lets both exist in peace. But it is also a classic source of confusion, because a person skimming the code sees the name and assumes it means the outer, more familiar thing, when locally it does not. The important point for our purposes is not whether shadowing is good style. It is that the spelling of a name tells you almost nothing on its own. You have to know which scope you are in to know which definition a use is bound to.
Binding: connecting a use to its definition
Binding, also called name resolution, is the act of connecting a particular use of a name to the exact declaration it refers to. It is the answer to the question: when the code says rate here, which of the several things called rate does it mean. Binding is where a list of names turns into a web of real connections, because once you know that this use of quote is bound to that specific function definition, you have drawn an edge between two concrete things rather than matched two identical strings.
Doing binding correctly means carrying the scope rules with you as you read. You keep track of which rooms you are inside, what each room has declared, and the order to search them. When you reach a use, you perform that outward search and record the winner. This is patient, exact work, and it is the difference between knowing that a name appears and knowing what it means. It is also language specific in its details, because languages differ on when a name becomes visible, whether a use can appear before its declaration, and how names cross from one file into another through imports.
Binding is also what makes ordinary tasks safe. Take renaming a function, which sounds like the simplest change there is. To do it correctly you must change the declaration and every use that is bound to it, and only those. If you change the declaration but miss a bound use, the program breaks because that use now points at nothing, or worse, at some other thing of the same name. If you change a use that was never bound to your function, a namesake in another scope, you have broken unrelated code. A rename done by spelling alone gets both wrong. A rename done by binding touches exactly the right set, no more and no less. Every reliable operation over a codebase, from renaming to finding callers to judging what a change could disturb, rests on having bound the uses to their definitions first.
The spelling of a name tells you almost nothing on its own. You have to know which scope a use is standing in to know what it is bound to.
Why the same name means different things
Pulling the pieces together: a single spelling can refer to many different things in one program, and this is not sloppiness, it is the design. Scopes exist precisely so that names can be reused without collision. A large system may contain dozens of functions called handle, dozens of variables called result, several classes called Manager or Client or Config, and the program runs correctly because each use is bound, by the scope rules, to exactly one of them.
Names also travel. When one file imports a name from another, it may bring it in under a different spelling, or bundle it under a namespace, so the same underlying thing wears different labels in different files. Two files can both use the name Client and mean entirely unrelated classes from unrelated libraries. The meaning lives in the binding, not in the letters, and the binding depends on scope, imports, and position.
There is a further wrinkle that trips up even careful readers. The same thing can be reached by more than one name at once. A file might import a function and immediately give it a shorter local alias for convenience, so both the original name and the alias are bound to a single definition. Elsewhere a method might be reached through a variable whose type is only known indirectly, so the name you see written is the variable’s, not the method’s. In all these cases the letters on the page and the thing they resolve to have quietly come apart. A person who reasons from the visible text alone will draw the wrong lines. Only by resolving each name through its scope and its imports do you recover the true picture of what is connected to what.
Why plain text search over a name is wrong so often
Now the payoff, and it is the whole reason this concept matters in practice. A text search knows nothing about scope or binding. It matches spelling. When you search a codebase for the name total, you get every place those five letters appear, and the results are a mixture of genuinely unrelated things.
In one search for a single common name you will typically catch:
- Real uses of the specific thing you meant.
- Uses of a completely different thing that happens to share the name, in another scope.
- The word inside comments, which do not run.
- The letters inside a longer word, so total matches subtotal and totally.
- The word inside strings of text that the program prints but never executes.
- Definitions and uses jumbled together, with no marker for which is which.
The search cannot separate these because separating them is exactly the work it does not do. It has no idea which room each match is standing in, so it cannot tell the local rate from the module rate, the function you care about from a namesake, or a real call from a mention in a comment. It gives you every string and leaves the binding to you. For a human reading a handful of results that is a manageable annoyance. For any tool trying to answer who calls this exact function, or what would break if I rename it, matching spelling is not almost right. It is a different question with a different, often misleading answer.
This is why serious code understanding does not rest on text search. It rests on the structure, walking the parsed tree, tracking scopes as it goes, and binding each use to the one definition the language rules say it refers to. Only then does a match mean what you wanted it to mean: not the same letters, but the same thing.
Where this sits in the larger picture
Symbols and scope pick up exactly where declaration extraction leaves off. Extraction finds the names a codebase defines and puts them on record. This concept is about what those names mean at each point they are used, and about binding a use to its definition rather than to a lookalike. That binding is the raw material for everything relational, because a call from one function to another is only real once you have resolved which function is calling and which function is being called.
The technique that carries scope rules through a whole program without ever running it is static analysis, the discipline of learning what code does by reading its structure. And once uses are correctly bound to definitions, the calls between functions can be assembled into the call graph, the map of who invokes whom, which is one of the most useful views a code knowledge graph can offer.
Connected concepts
Where this sits
