Skip to content

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.