Summary
The line
code = compile('l = list(1, 2, 3, 4, 5)', '', 'exec')
is valid because list is being called as a normal function, and the Python interpreter does not check the signature of the called object at compile time. The compiler simply emits a CALL_FUNCTION opcode with the number of positional arguments it sees, leaving the runtime to raise a TypeError if the call is illegal.
When the same text uses subscript syntax (list[1, 2, 3, 4, 5]), the parser treats it as subscription, not a function call, and generates BINARY_SUBSCR. This explains the different bytecode.
Root Cause
- Parser ambiguity:
list(→ function call;list[→ subscription. - Compile‑time laziness: The compiler records the number of arguments but does not verify that the target object is callable with that arity.
- Runtime enforcement: The actual
TypeError(“list() takes at most 1 argument (5 given)”) is raised only when the bytecode is executed.
Why This Happens in Real Systems
- Python’s dynamic nature means the compiler cannot know the type of
listat compile time; it could be rebound to a custom callable. - The AST → bytecode pipeline is intentionally thin: it translates syntax directly to operations (
CALL_FUNCTION n) without semantic checks beyond syntax errors. - Performance considerations: Adding full signature validation during compilation would drastically increase compile time for scripts that are never executed.
Real-World Impact
- Silent bugs: Typos like
list(1, 2)compile but fail at runtime, potentially hiding errors in large codebases until the offending path is exercised. - Security surface: Attackers can craft code that compiles but raises exceptions only later, possibly interfering with error‑handling logic.
- Tooling confusion: Static analysis tools that rely only on compile‑time information may flag code as correct even though it will crash at runtime.
Example or Code (if necessary and relevant)
code = compile('l = list(1, 2, 3)', '', 'exec')
exec(code) # → TypeError: list() takes at most 1 argument (3 given)
code2 = compile('l = list[1, 2, 3]', '', 'exec')
exec(code2) # → TypeError: 'list' object is not subscriptable
How Senior Engineers Fix It
- Run linters/static checkers (e.g., pylint, flake8, mypy) that flag calls to built‑ins with an incorrect number of arguments.
- Add unit tests that cover edge‑case argument patterns, ensuring the exception is exercised early.
- Enable
-Werroror fail‑fast policies in CI pipelines so that anyTypeErrorraised during test execution breaks the build. - Avoid rebinding built‑ins in module scope; keep the original
listuntouched to reduce confusion.
Why Juniors Miss It
- They assume compile‑time guarantees similar to statically typed languages, overlooking Python’s runtime‑only type checking.
- They often mix up syntactic forms (
()vs.[]) and treat them as interchangeable, not realizing they map to entirely different bytecode operations. - Lack of exposure to bytecode inspection (
dismodule) leads to the false belief that the compiler validates argument counts.