DEV Community

Taylor Wang
Taylor Wang

Posted on

The New File Was Named types.py. The Free Server Imported a Different Module.

Have you ever shipped a refactor that looked smaller than a coffee run and then spent two days proving Python can lie? I asked a free model to lift a handful of dataclasses out of a growing worker module. It did the extraction cleanly enough, except it named the new file types.py, which is a loaded word in this language. Local pytest stayed green. The same tree on a free server started failing on an import that I did not even touch.

I was comparing those two environments with MonkeyCode because free model access and a free server option let me rerun the same commands without inventing a second laptop. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The product is not the bug. The bug is that import types is a sentence Python will happily interpret in two incompatible ways.

What I thought was happening

The first error on the server mentioned SimpleNamespace, which I had not edited. So I blamed packaging, then pinned typing_extensions, then reinstalled the project in a fresh virtualenv. Does that sound familiar? You chase the noisy name in the traceback and ignore the quiet name sitting in sys.path[0].

I also blamed the model for “hallucinating an API,” which was unfair in a specific way. types.SimpleNamespace is real. The server was not missing the standard library. It was importing my types.py first, and that file had dataclasses and no SimpleNamespace at all.

Hours 0–8: I debugged the wrong layer

I started with the boring story every remote failure invites. Was the interpreter different? Was pytest collecting a different tree? Had an __init__.py gone missing in the copy step?

What I actually ran, reconstructed here as a lab you can repeat:

# local, from the repo root
python -c "import sys, types; print(sys.executable); print(types.__file__)"
pytest -q

# later, on the free server, from a different working directory
cd app
python worker.py
Enter fullscreen mode Exit fullscreen mode

The local command printed a types.py under the standard library, and the suite passed. The server command printed a types.py sitting next to worker.py. Same repo. Same commit. Different current working directory, so sys.path[0] changed meaning.

I wasted those first hours on dependency noise. The model had also rewritten a couple of imports to the short form from types import SimpleNamespace, which is correct English and a trap the moment a local file steals the name.

Hours 8–24: the “fix” made the lie more stable

I asked the free model how to make the import deterministic. It suggested sys.path.insert(0, ...) in worker.py, which is the kind of one-liner that feels like control. I did not paste it into production code. I did paste it into a scratch file to see what it would do.

It “fixed” the server by forcing the application package onto the front of the path. It also made import types resolve to my helper module in both environments, so pytest finally failed locally too. That was the first honest result of the whole incident. Have you noticed how a bad path mutation can look like progress because it equalizes the wrong outcome?

What broke when I tried that path insert:

  • pytest started importing app.types when a fixture still expected the stdlib module.
  • A debug snippet using types.MappingProxyType raised AttributeError.
  • python -m pytest and pytest disagreed, because one of them changed sys.path[0] relative to the test files.

I reverted the insert. The honest fix is rename the helper, not win a fight with the import system.

Hours 24–48: I wrote a detector instead of another prompt

The useful artifact was not another generated patch. It was a tiny receipt I now run in the same working directory as the failing command. If the printed path is inside your repo, you do not have the standard library module you think you have.

# save as tools/which_module.py
# reconstructed lab script, not a captured production log
from __future__ import annotations

import argparse
import importlib
import sys
from pathlib import Path

DANGEROUS_NAMES = (
    "types",
    "code",
    "email",
    "json",
    "logging",
    "test",
    "token",
    "copy",
    "parser",
    "random",
    "site",
    "stat",
    "string",
    "typeshed",
)


def describe(name: str) -> dict[str, str]:
    module = importlib.import_module(name)
    file_path = Path(getattr(module, "__file__", "") or "")
    cwd = Path.cwd().resolve()
    try:
        relative = str(file_path.resolve().relative_to(cwd))
        location = "inside_cwd"
    except ValueError:
        relative = str(file_path)
        location = "outside_cwd"
    return {
        "name": name,
        "file": relative,
        "location": location,
        "cwd": str(cwd),
        "sys_path_0": sys.path[0],
        "executable": sys.executable,
    }


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("names", nargs="*", default=list(DANGEROUS_NAMES))
    args = parser.parse_args()
    rows = [describe(name) for name in args.names]
    shadowed = [row for row in rows if row["location"] == "inside_cwd"]
    for row in rows:
        mark = "SHADOW" if row["location"] == "inside_cwd" else "ok"
        print(f"{mark:6} {row['name']:10} {row['file']}")
    print(f"cwd={rows[0]['cwd']}")
    print(f"sys.path[0]={rows[0]['sys_path_0']}")
    print(f"executable={rows[0]['executable']}")
    return 1 if shadowed else 0


if __name__ == "__main__":
    raise SystemExit(main())
Enter fullscreen mode Exit fullscreen mode

Run it twice, because one passing environment is how this class of bug hides:

python tools/which_module.py types code email json logging test
echo "exit=$? cwd=$(pwd)"

cd app
python ../tools/which_module.py types
python -c "import types, inspect; print(types.__file__); print('SimpleNamespace' in dir(types))"
Enter fullscreen mode Exit fullscreen mode

If the first command prints ok and the second prints SHADOW, you are not looking at flaky tests. You are looking at two different modules that share a four-letter name.

A minimal tree that reproduces the lie

This is a reconstructed fixture, not a claim about a private codebase. Drop it in an empty folder and you can watch the split happen without any model in the loop.

proj/
  app/
    worker.py
    types.py
  tests/
    test_worker.py
  tools/
    which_module.py
Enter fullscreen mode Exit fullscreen mode
# app/types.py
from dataclasses import dataclass

@dataclass(frozen=True)
class JobSpec:
    name: str
    timeout_s: int
Enter fullscreen mode Exit fullscreen mode
# app/worker.py
from types import SimpleNamespace

def build_defaults() -> SimpleNamespace:
    return SimpleNamespace(retries=2, backoff_s=1.5)

if __name__ == "__main__":
    print(build_defaults())
Enter fullscreen mode Exit fullscreen mode
# tests/test_worker.py
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "app"))

from worker import build_defaults

def test_defaults_have_retries():
    assert build_defaults().retries == 2
Enter fullscreen mode Exit fullscreen mode

Now run the two entry points:

cd proj
pytest -q                 # often green: tests may still find stdlib types
cd app && python worker.py  # ImportError: cannot import name SimpleNamespace
Enter fullscreen mode Exit fullscreen mode

Why the split? Because script execution puts the script’s directory at sys.path[0]. A test runner started from the repo root may not. Free models love short stdlib imports. They also love extracting “types” into types.py. Those two habits collide as soon as cwd changes, which a free server will do the moment your start command is not the command you run at your desk.

Decision table I wish I had at hour one

Symptom you see Do not start with Check first
ImportError: cannot import name SimpleNamespace from types reinstalling pytest python -c "import types; print(types.__file__)" in the failing cwd
AttributeError: MappingProxyType after a “harmless” extract pinning typing stacks a repo file named types.py or types/__init__.py
CLI fails, pytest passes blaming collection count sys.path[0] under python worker.py versus pytest
pytest fails, CLI passes blaming the server image a sys.path.insert the model added “to be safe”
Only the free server fails assuming a missing OS package print cwd, argv0, and types.__file__ in the job itself
Failures flip when you cd network, rate limits, “the model drifted” directory layout plus script versus -m execution

Keep the table next to the detector. The point is to spend ten minutes on identity of the module before you spend ten hours on identity of the environment.

What I would repeat

I would still use a free model to do the mechanical extract. The extract was fine. The filename was not. I would still run the same tree on a free server, because laptop pytest is a terrible witness for sys.path[0].

What I would repeat, as a checklist rather than a vibe:

  1. Ban helper filenames that match stdlib modules before the patch lands.
  2. Print module.__file__, cwd, and sys.path[0] inside the failing job, not in a later shell.
  3. Prefer python -m app.worker from the repo root over cd app && python worker.py.
  4. Treat sys.path.insert suggestions as a smell, even when they make one environment go green.
  5. Rename first. Reinstall second. Prompt third.

Would I ask the model to name files again? Yes, and I would paste the dangerous-name tuple into the prompt. Models are good at moving dataclasses. They are not good at remembering that types, code, email, and test already belong to the language.

Limitations, and who should skip this

This workflow is for import identity bugs, not for missing binaries, not for drifting model output, and not for tests that collected the wrong file. If your failure is an exit code from a command that is not on PATH, a module __file__ print will not save you.

Do not use a free server for secrets, production data, or credentials just to get a second cwd. Do not treat free model access as a pinned compiler: it can rename files, invent path inserts, and still sound confident. Do not run the detector as proof of a compromised machine; a shadowed stdlib name is usually an embarrassing filename, not an incident.

Skip the approach if you cannot execute the same argv in both places. Skip it if your packaging already forbids loose scripts and you only ever run python -m. Skip it if the traceback is inside C extensions or a binary wheel, because __file__ on types will look innocent while a different module is crashing.

The 48 hours were not a model failure in the cinematic sense. They were a working-directory failure wearing a generated filename. After the rename from types.py to job_types.py, both environments imported the same universe again. If you are about to extract helpers, ask one rude question first: what happens to import types when the server starts one directory to the left?

Top comments (0)