11 / 12

Why does Python not require explicit variable declarations, and how does dynamic typing affect variable assignment internally?

Difficulty: 3/10
Variables, Data Types & Memory Model, Dynamic Typing, Name Binding and References

Names as bindings and dynamic typing

In Python a variable is a name bound to an object, not a typed box that holds a value. The type lives on the object (every object carries a pointer to its type), never on the name. So there is nothing to declare: the assignment statement itself creates or rebinds the name. x = 10 creates an int object (or reuses a cached one) and binds x to it. x = 'hi' later just points x at a different object; the int is untouched and is freed only if nothing else references it.

Internally, in CPython the compiler decides the storage class of each name per scope. Locals in a function live in a fast array on the frame (STORE_FAST), module and class-level names live in a dict (STORE_NAME / STORE_GLOBAL). Binding writes a pointer and adjusts reference counts. Type checks happen at operation time: x + y dispatches to type(x).add and falls back to type(y).radd, raising TypeError if neither handles it. That is why it is called dynamic (types resolved at run time) but strong (no silent coercion of str + int).

javascript

Trade-offs: dynamic typing gives flexibility, less boilerplate and easy duck typing, but moves a class of bugs from compile time to run time and makes large refactors riskier. At scale I'd pair it with type hints plus mypy or pyright in CI, accepting that hints are not enforced at runtime (use pydantic or beartype if you need runtime validation). Statically typed languages can also optimize better; CPython 3.11+ partly closes the gap with the specializing adaptive interpreter, which speculates on types seen at each bytecode site.

Common mistakes: believing b = a copies the data, believing assignment 'changes the variable's type' (it rebinds to a different object), and being surprised by UnboundLocalError - if a function assigns to a name anywhere, that name is local for the whole function, so reading it earlier fails. That error is a good demonstration that name scoping is decided at compile time even though types are decided at run time.

Scenario Questions

0-2 years experience

  1. 1Predict the output and explain: x = 10; y = x; x = x + 1; print(y) - then l1 = [1]; l2 = l1; l1.append(2); print(l2). Why do the two cases behave differently?
  2. 2A variable first holds an int and later a string. Is that legal? What happens if you then call a method that doesn't exist on it, and when is the error raised?

2-5 years experience

  1. 1A function annotated def f(x: int) is called with a string in production and runs fine until it crashes much later. Why didn't Python catch it, and what would you add to your workflow?
  2. 2A function reads a variable on its first line and assigns to it on its last, and raises UnboundLocalError. What does this tell you about how Python resolves names, and how would you fix it?

5-8 years experience

  1. 1Lambdas created in a for loop all return the last loop value. Explain this through name binding and show at least two fixes.
  2. 2You are introducing gradual typing into a 200k-line untyped codebase. How would you roll it out, and what does mypy or pyright not guarantee at run time?

8+ years experience

  1. 1Explain how the specializing adaptive interpreter in CPython 3.11+ depends on type stability at a bytecode site, and how that should (or should not) influence how you write hot-path code.
  2. 2A hot loop is slow because of repeated global lookups and attribute access. How would you profile it, and which binding-related optimizations (local aliasing, __slots__) are worthwhile versus premature?

Follow-up Questions

  • How is Python 'strongly typed' even though it is dynamically typed?
  • Why does assigning to a name anywhere in a function make it local for the entire function?
Share

Share via WhatsApp, X, Facebook, LinkedIn or copy link. Open Graph preview enabled.