MLC000: Python source cannot be parsed¶
qual reports MLC000 when a selected Python source cannot be decoded,
tokenized, or parsed for the configured target-python version.
- Default severity:
error - Minimum confidence:
certain - Implementation phase:
0 - Fix: none
A file that cannot be decoded or parsed does not proceed to semantic analysis, but analysis of every other selected file continues. Locations use one-based Unicode character columns, including when non-ASCII text occurs before the syntax error.
Pre-parse admission limits¶
Analysis never executes the source, but parsing and several analysis passes
recurse over its structure. A source nested far past anything a person writes
would drive the recursive-descent parser past the native stack and abort the
whole process — turning "one file is skipped" into "the tool, or the service
that invoked it, died". Three limits are therefore checked before parsing and
reported as MLC000:
| limit | bound | rejected input |
|---|---|---|
| source size | 4 MiB | generated or concatenated files whose parse time is unbounded |
| bracket / indentation nesting depth | 96 | VGroup(VGroup(...)) or if chains nested past the stack |
consecutive prefix operators (-, +, ~, not) |
64 | x = ---...---1, including runs split across line continuations |
The bounds sit far above real Python: the Manim Community sources peak at bracket depth 15 and 13 indentation levels. A file over a limit is skipped with its bound stated in the message; every other selected file is still analyzed. Because the lexer is iterative, measuring the token stream is safe even for input the parser could not survive.
target-python syntax gate¶
The bundled parser (rustpython-parser 0.4) always parses with its fixed
Python 3.12 grammar and has no feature_version pinning, so a construct
newer than the configured target-python parses fine. After parsing,
qual walks the AST, the token stream, and the raw text of f-string
tokens and reports every such construct as MLC000 ("X requires Python
3.Y but target-python is 3.Z"), spanned on the construct. Unlike a parse
failure, a gated file keeps its AST: the file itself and every other file
are still fully analyzed — the gate only adds the diagnostic. A --fix
whose edits would introduce gated syntax (a gated construct occurring
more often than in the original text) rolls the whole file back.
The gate's contract: a file it passes silently is parseable by the
target version's own parser (ast.parse). Coverage is complete for
every target from 3.6 (the configuration floor; older targets are
refused with exit 2) to 3.12; each minimum version was checked against
CPython 3.12's ast.parse(feature_version=...) oracle, and constructs
that oracle does not gate are dated from their PEP / release notes:
| construct | min | detection | oracle |
|---|---|---|---|
async/await syntax outside async def (await, async for/with, async comprehensions) |
3.7 | AST | feature_version-verified |
:= assignment expression (PEP 572) |
3.8 | AST | feature_version-verified |
positional-only / (PEP 570), incl. lambdas |
3.8 | AST | feature_version-verified |
f-string self-documenting = (f"{x=}") |
3.8 | f-string text (the AST is identical to f"x={x!r}") |
not fv-gated; 3.8 release notes |
| relaxed decorator expressions (PEP 614) | 3.9 | token stream | not fv-gated; PEP 614 |
parenthesized context managers with as |
3.9 | token stream | not fv-gated; accepted by 3.9's PEG parser, documented in 3.10 |
match statement (PEP 634) |
3.10 | AST | feature_version-verified |
except* handler (PEP 654) |
3.11 | AST | feature_version-verified |
* unpacking in subscripts / *args annotations (PEP 646) |
3.11 | AST | not fv-gated; PEP 646 |
type alias statement (PEP 695) |
3.12 | AST | feature_version-verified |
type parameter list on def / class (PEP 695) |
3.12 | AST | feature_version-verified |
f-string expression with a backslash, newline, or # (PEP 701) |
3.12 | f-string text | not fv-gated; PEP 701 |
Outside the contract (honestly NOT gated): version-dependent
compile-stage restrictions that every affected version's parser
accepts — continue inside finally (compile error before 3.8) and the
placement rules for await/async comprehensions inside nested
comprehensions (relaxed in 3.11). PEP 701 forms the bundled parser
rejects (f-string quote reuse) are ordinary MLC000 parse errors.
Targets below 3.6 are configuration errors because Python 3.6's own
additions (f-strings, numeric-literal underscores, variable annotations,
async generators/comprehensions, signature trailing commas) include
constructs the oracle cannot verify, so a sub-3.6 guarantee would be a
promise qual cannot keep.
The reverse direction: valid 3.6 source the bundled grammar rejects¶
The gate above catches newer-than-target constructs; the opposite
mismatch also exists. async and await were soft keywords until
Python 3.7, so source that uses them as identifiers (async = 1,
def await(): ...) is valid for a 3.6 target but fails to parse under
the bundled 3.12 grammar, where they are reserved. When a file fails to
parse, the configured target-python is below 3.7, and the failing line
(or the parser's message) mentions async or await as a word, the
parse-failure MLC000 message gains a note:
(note: this may be valid Python 3.6 source; qual parses with the
3.12 grammar, where `async`/`await` are reserved keywords)
This is deliberately a hedged hint, not a claim — the file may be broken for unrelated reasons — and it never changes the diagnostic's severity, span, or the fact that the file is skipped.
Encoding declarations¶
PEP 263 encoding declarations accept both WHATWG labels and CPython codec
names/aliases (latin-1, cp932, koi8_r, latin5, thai, ...),
and follow CPython's exact cookie placement rule: the cookie counts on
line 1, or on line 2 only when line 1 is blank or comment-only — a cookie
after a code line is dead text. The ISO-8859 family decodes byte-for-byte
like CPython, including iso-8859-9 and iso-8859-11, whose WHATWG
counterparts (windows-1254 / windows-874) differ from CPython only in the
0x80–0x9F C1 range and are decoded with that range mapped to U+0080–U+009F.
A declared codec that CPython knows but the linter cannot represent is
reported as "not supported by qual" — an explicit linter limitation,
never a claim that the target Python would fail to decode the file.