os has no rmdir
import os
os.rmdir(d) # error: undefined variable (os)
os.path.isdir(d) # fine
os.listdir("/tmp") # fine
os.sep # fine
os.getpid() # error: undefined variable (os)
The diagnostic is misleading and that is the expensive part
undefined variable (os) names the MODULE when the module is bound and working
three lines earlier. Anyone reading it looks at the import first. The message for
a genuinely missing member of a bound qualifier should look like the one
tempfile gives — no member mkdtemp came of the qualifier tempfile — which is
precise and sends the reader to the right place. shutil.rmtree produces exactly
that better form, so the two diagnostics disagree about the same class of gap.
Not the same as os.remove / os.makedirs
Those exist as compiler-provided calls and refuse only as bare values:
print(os.remove) -> Nil Python: os.remove is a compiler-provided ...
print(os.makedirs) -> Nil Python: os.makedirs is a compiler-provided ...
Do not "fix" this ticket by finding those and concluding the family works.
Why it matters beyond tidiness
tempfile.TemporaryDirectory is the natural companion to the mkdtemp landed on
2026-09-12 and cannot be written without a directory removal. Any NilPy program
that makes a scratch directory currently has no way to clean it up.
Log
- 2026-09-12 — resolved; this names the commit that carried the resolve, which is not always the one that carried the change — commit d0d490ad2.
How it was fixed (2026-09-12)
Three places, and the cheapness is the point:
compiler/builtin/pypal.pas—PyPalRmdir, reusingNR_UNLINKATwithPYPAL_AT_REMOVEDIR = 512. Linux has no at-familyrmdir, sounlinkat(AT_FDCWD, path, AT_REMOVEDIR)is the syscall, and no number had to be added to the six per-targetNR_*tables. That is the whole reason this was same-day work andos.getpid()was not.compiler/builtin/pylib.pas—pyos_rmdir, raising throughpyos_raise_ioerroron a negative return so a non-empty directory or a missing path surfaces as an exception rather than a quiet rc.compiler/pyparser.inc— one row in the dotted stdlib table,os.rmdir -> pyos_rmdir.
Deliberately non-recursive, matching CPython: rmdir on a non-empty
directory raises. The fixture asserts both halves (NONEMPTY raised True,
MISSING raised).
It is a BUILTIN addition, so it is inert until a pin
compiler/builtin/*.pas is resolved CWD-relative by the live compiler, so this
works immediately in the dev loop and is invisible to every
$(PXX_STABLE) consumer until the owner pins. make pin is owner-only; this
is recorded here rather than waited on.
Residuals, each with an owner
os.getpid()— still absent, same misleading diagnostic. Split out as [[bug-n-os-has-no-getpid]] (track N, p30) because it needs six correct per-target syscall numbers where this needed none.shutil.rmtree— still absent (no member rmtree came of the qualifier shutil), sotempfile.TemporaryDirectorystill cannot be written. That is track B / lib/rtl, not this ticket.
What it retired
This ticket's own summary used to say the mkdtemp fixture "has to leave two
empty directories behind every run and says so". It does not any more — it
cleans up with this rmdir and measures delta 0 inodes over three runs. The
summary was corrected in the same commit, not left for a later reader.