Topic 03

Names, Scopes and Types

Languages

year>"abc" is a perfectly shaped query: a field, a comparison, a value. It is also nonsense, because years are numbers. auther:"le guin" is shaped right too, and names a field that does not exist. Both parse without complaint, and both would reach the index if nothing stopped them.

Catching them before Lantern touches the index is the job of the stage between parsing and running: resolve every name to what it means, and check that every operation fits the types it touches. Programming languages do the same with variables and functions. Whether they do it before the program runs or while it runs is the difference between static and dynamic typing, and each choice has a price paid in a different place.

The Symbol Table

A symbol table maps each name to what it means. Lantern's is fixed at five entries. Title, author and subject are text fields, year is a number, and branch takes one of the library's 30 branch names. Resolving a name is a dictionary lookup, the operation the hash tables topic in Chapter 5 makes cheap.

FieldTypeWhat it reads
titletextpostings lists for title terms
authortextpostings lists for author terms
subjecttextpostings lists for subject terms
yearnumbereach record's year
branchone of 30 nameswhich records a branch holds

When a lookup fails, the table can do better than "unknown field". auther is one edit away from author, by the edit distance Chapter 9 computes with dynamic programming, so Lantern's checker says: unknown field "auther"; did you mean "author"? The error points at the position of the misspelled word, which the lexer kept on its token.

Checking the Tree

The checker is one walk over the syntax tree from the previous topic, asking at each node whether the operation fits its operands. A phrase on a text field is fine. A greater-than with a number on year is fine. year>"abc" compares a number field with text and is rejected. author>1970 orders a text field and is rejected. branch:"atlantis" names no branch and is rejected.

The comparison case of Lantern's checker, and the messages it produced on Python 3.15
        case Compare(field, op, value, kind, pos):
            ftype = resolve(field, pos)
            if ftype != "number":
                raise CheckError(f"{field} is {ftype}, and {op} compares numbers", pos)
            if kind != "NUMBER":
                raise CheckError(f'{field} is a number, but "{value}" is text', pos)

# auther:"le guin"    unknown field "auther"; did you mean "author"?
# year>"abc"          year is a number, but "abc" is text
# author>1970         author is text, and > compares numbers
# branch:"atlantis"   no branch is named "atlantis"

The excerpt handles comparison nodes. It resolves the field through the symbol table, which raises the unknown-field error if the name is not there, then insists that the field is a number and that the value is one too. Under it are the four messages the checker printed for the four bad queries in this topic. Each names the problem in the user's terms and carries a position, so the search box can underline the right word.

The walk over a tree this small takes microseconds. The evaluation it prevents reads postings lists. And a clear error beats the alternative, which is an empty results page that leaves the user guessing whether the library has no such books or the query was wrong.

Scopes

In a programming language, names are not fixed. A scope is a region of code in which a name means one thing, scopes nest, and looking up a name walks outward through a chain of symbol tables. In Python the chain is the current function, any enclosing function, the module and finally the built-ins.

A name used in an inner function, looked up from the inside out
built-ins: len, print, sum …module: CATALOGUE_SIZE, lookupfunction search(query): terms, limitinner function score(record): weighta name used in score1234

The figure nests four scopes. A name used inside the inner function score is looked up in score first, then in the function around it, then in the module, then among the built-ins, and the first match wins. CPython decides which names are local when it compiles the function, not when it runs it. A name assigned anywhere in a function is local throughout that function.

That rule produces a classic surprise. A function that prints a module-level total and later assigns to total fails on the print, before the assignment is ever reached. On Python 3.15 the message is: cannot access local variable 'total' where it is not associated with a value. The exception is UnboundLocalError, and it exists because scope was fixed at compile time.

Static and Dynamic Typing

Static checking reads the program text before it runs and rejects mismatches: Java, Go, Rust, TypeScript and Python under an external type checker. Dynamic checking attaches a type to every value and checks it at each operation as the program runs. CPython inspects both operands' types on every addition to decide which addition to perform.

Python's type hints, standard since Python 3.5 through PEP 484, sit between the two. The interpreter stores them and ignores them at run time. External checkers read them before the program runs and report mismatches, and libraries that validate data at a boundary can read them while it runs. A hint is a promise someone else checks.

What a Checker Proves and What It Cannot

A type checker proves that one class of error cannot happen: no operation receives a value of the wrong kind. It does not prove the program right. A function that returns year minus one where year plus one was meant type-checks. So do a division by zero, an index past the end of a list and a loop that never finishes. Chapter 14 shows that no checker can decide every behaviour of every program.

Every checker therefore leans one way. A sound checker rejects anything it cannot prove safe, and so rejects some correct programs. A permissive one accepts what it cannot disprove, and so lets some errors through. TypeScript and Python's Any type are permissive on purpose, to let untyped code in without rewriting it.

What Checking Costs

Static checking moves the price to before the program runs: the effort of writing annotations and the checker's time on every build, repaid by errors found in the editor instead of in production. Dynamic checking pays on every operation, as part of the interpretation cost the next topic measures.

Names cost at run time too. In CPython a local variable is a slot in an array, read by index, while a global is a dictionary lookup. In a measurement on Python 3.15, a loop of a million additions that read a global ran about 10 percent slower than the same loop reading a local. The gap is that small partly because, since Python 3.11, the specializing interpreter covered in the last topic of this chapter caches where a global was found.

Static vs Strong Typing

Static vs dynamic is when types are checked: before running or during. Strong vs weak is whether the language converts between types silently. The two axes are independent.

Python is dynamic and strong: adding the string "1" to the number 1 raises a TypeError, "can only concatenate str (not "int") to str". JavaScript is dynamic and weak: the same expression gives the string "11". C is static and weak, since casts and implicit conversions reinterpret bytes. Rust is static and strong. "Python has no types" is wrong on both axes.

Misconceptions
  • "Static typing catches all bugs." It catches values of the wrong kind. A wrong formula, an off-by-one, a missing case and a slow query all type-check, which is why typed codebases still need tests.
  • "Dynamically typed means untyped, and Python is weakly typed." Every Python value carries its type and every operation checks it. Adding a string to a number is an error in Python and a string in JavaScript.
  • "Type hints make Python check types when the code runs." The interpreter stores hints and ignores them. Only a type checker, or a library that validates at a boundary, acts on them.
  • "Python looks names up line by line as it runs, so a function can read a global and then assign it." Scope is decided at compile time. The assignment makes the name local for the whole function, and the earlier read fails with UnboundLocalError.
  • "Code that passes the type checker cannot fail with a type error." Any, casts, untyped libraries and deliberate gaps in soundness let wrong types through. A passing check covers what it saw.
Why It Matters
  • Resolve names and check types before any expensive work. Lantern rejects a year compared with text before it reads a single postings list.
  • Report a name error with its position and the closest valid name. Edit distance turns "unknown field" into a fix.
  • Type the boundaries first. Function signatures at module and service edges are where a value of the wrong kind travels furthest before it fails.
  • Treat a passing type check as proof that one class of error is absent, not that the code is right. Keep tests for behaviour.
RelatedParsing checks shape, where this stage checks meaning (previous topic)Rice's theorem why no checker is both sound and complete about behaviour (Chapter 14)Run-time validation a library checks data at a service boundary while the program runs (Backend Deep Dive)

Knowledge Check

Which stage of Lantern rejects each of auther:x, year>"abc" and AND AND year?

  • The lexer rejects all three, since each of them contains an error
  • The parser rejects all three, since each is an invalid query
  • The checker rejects the first two queries; the parser rejects the third one
  • The checker rejects all three, since each one has an unknown name in it

A module sets total = 0. A function prints total and then assigns total = 5. What happens when it is called?

  • It prints 0, and then sets a separate local variable called total to 5
  • The print fails with UnboundLocalError before the assignment runs
  • It prints 0, and then changes the module-level variable total to 5
  • It fails at import time with a SyntaxError that names the variable

Where do Python, JavaScript and Rust sit on the static/dynamic and strong/weak axes?

  • Python dynamic and weak; JavaScript dynamic and weak; Rust static and also strong
  • Python static and strong; JavaScript dynamic and weak; Rust static and weak
  • Python dynamic and strong; JavaScript dynamic and weak; Rust static and strong
  • Python dynamic and strong; JavaScript static and weak; Rust static and strong

Why does reading a local variable cost less than reading a global in CPython?

  • A local is a slot in an array read by index; a global is a dictionary lookup
  • A local lives in a processor register; a global lives in main memory
  • A global must be locked before every read, in case another thread writes it
  • A local is checked once at compile time; a global is type-checked on every read

A codebase passes a sound type checker with no errors. What has that proved?

  • That the program computes the right results for every possible input
  • That no exception can ever be raised at run time by any function
  • That every loop in the program terminates on every possible input
  • That no operation receives a value of a kind it cannot handle

You got correct