← board

from sys import X fails while import sys works

Repro

import sys              # ok
from sys import argv    # error: import: no unit named sys and no shim mimic_sys

The boundary

import result
import sys OK
from sys import argv / version_info error: no unit named sys
import os OK
from os import getcwd error: no unit named os
from math import pi OK
from time import time OK
from six import text_type (a mimic_ shim) OK

So the from-import path resolves .py shims and whatever provides math and time perfectly well. What it does not resolve is the set that sys and os belong to — the modules the compiler provides directly. Whichever table the plain-import path consults for those, the from-import path is not consulting it. (Hypothesis, not measured: nothing here read the resolver.)

Do NOT close this by writing a mimic_sys.py

That was the first thing that came to mind and it is the wrong fix twice over: sys is already provided — it would be a second, competing sys — and it would hide the resolver gap rather than close it, which is what devdocs/dev/normalise-dont-special-case.md is about. The two import spellings are two shapes of one concept; the fix belongs where they diverge.

What it costs today

html5lib/_tokenizer.py does from sys import version_info and stops there — one row of the ladder table (missing module: sys), on the file that is the tokenizer.

Related, and probably the same fault line: sys.version_info COMPILES and then raises at runtime with an explicit "this build has no sys.version_info: the import it came from could not be resolved" — so someone has already met the resolver gap from the other side and put a loud failure there rather than closing it.

RESOLVED 2026-08-18 (frank2-7e) — one table, not three

Re-measured at HEAD first: reproduced exactly as filed, then found it was wider in both directions than the ticket knew.

Root cause: duplication, not a resolver gap

One concept — "this root has no unit and no shim behind it, so consume the import and let an unsupported name wall at its USE site" — existed in three hardcoded copies: PyImportRootIsConsumedOnly for from X import a, plus two inline CaseEqual chains in the plain import X path. The copies had drifted:

plain import X from X import n
sys, os, textwrap, select ok FAIL
dataclasses, __future__ FAIL ok
typing, itertools ok ok

Six roots, not the two reported — and the reverse direction was unreported entirely. The ticket's hypothesis ("the from-path is not consulting whatever table the plain path uses") was right in spirit and wrong in shape: both paths have a table, and neither is authoritative.

Checked on disk rather than assumed: none of sys, os, textwrap, select, typing, itertools, dataclasses, __future__ has a unit or a mimic_ shim anywhere. random, collections and math DO (lib/rtl/*.pas) — which is why they resolve either way and why this went unnoticed.

Fix

PyImportRootHasNoBackingUnit is the single source of truth. Each spelling names the one root it genuinely treats differently, at the site where the reason is visible: collections is consumed by the from-spelling but must NOT be by the plain one (or collections.Sym has no unit to resolve against); random is the mirror case. The next root now has to be added once.

22 cells (11 roots x 2 spellings) green at HEAD; 6 fail on pinned v349. test/test_nilpy_import_spellings.npy imports every root both ways in one file, so adding a root to one path and forgetting the other fails in test-nilpy instead of in a corpus scan months later. Wired by name into test-nilpy and test-core (neither globs). gate.sh quick GREEN.

Explicitly NOT a ladder move

html5lib/_tokenizer.py does from sys import version_info (line 6) and USES it at line 21. It now advances from an import wall to an undefined-variable wall on the same name. Moved ONTO the next wall, not past it — the compile count does not change. Reporting it as ladder movement would be wrong.

On the title's premise — measured, and the correction needs correcting

The coordinator flagged mid-session that "import sys works" is false because sys.version_info throws at runtime, and suggested the honest shape might be "sys is unimplemented, fails three ways" (possibly Track B, write a shim). Measured, and that generalises too far:

import sys; sys.argv works (returns 1)
import os; os.getcwd() works
import sys; sys.version_info throws — that MEMBER is missing
from sys import argv; argv import resolves now; argv undefined

So sys is implemented and its members work; one member is missing. The premise "import sys works" is true for real members, and the failing case was generalised from a single absent one. No shim is wanted — sys is compiler-provided, and adding mimic_sys would create a second competing sys, exactly as this ticket warned in its own "do NOT close this by writing a mimic_sys" section. Stays Track N; lane unchanged.

Two separate defects this exposed — filed, not folded in

Neither belongs here: this ticket was the resolver divergence and that is closed.

Log