TRENDING
A vintage glass syringe with a metal plunger and orange measurement marks lying on a plain white background
November 9, 2026
How to Stop Command Injection in Python subprocess Calls, Including Git Option Injection and the Windows .cmd Trap
A green valet key and a black ignition key with a remote fob on a white background, the brand emblems blurred
November 9, 2026
Cloudflare’s Trusted Agent Delegation Essay Names 14 Site Problems, but the Identity Standard It Prefers Defines No Delegation, and Its First Shipped Fix Sees Agents Only at Login and Sign-Up
Sepia 1900 postcard photograph of two riders on horses galloping neck and neck at the front of a field on a Hannover racecourse, with the rest of the pack behind them
November 9, 2026
CloudLinux Says an Exploit Hit a CloudLinux 8 Server on November 4 Through Linux Kernel Bug CVE-2026-98276, Fixed Upstream Since September, and Red Hat Still Lists Every Supported RHEL Kernel as Affected
Two yellow commuter trains stand side by side on separate tracks next to a platform in a railway yard, with more tracks running in parallel on both sides and trees in the background
November 9, 2026
How to Run Parallel AI Coding Agents With git worktree and Check Their Branches for Conflicts Before You Merge
Car speedometer dial with a red needle pointing at about 60 miles per hour, or about 100 kilometers per hour on the inner scale
November 9, 2026
METR’s Anthropic Report Turns AI-Led Acceleration Into a 1.9x Slope Ratio That Reproduces From Its Charts, but the Ratio Runs From 1.1x to 2.9x With the Fit Function, and a Second METR Report Says Anthropic’s Public Risk Report Understated the Pace
09 Nov 2026
SXZ.io SXZ.io
  • Home
Search the Site
Popular Searches:
Technology Amazon AI
Recent Posts
Tall orange and white container cranes rising against a clear blue sky at a container terminal
How to Fix Python ModuleNotFoundError and Relative Import Errors, Break Circular Imports, and Stop Local Files From Shadowing the Standard Library
November 8, 2026
The Alcatraz lighthouse at night with beams of light sweeping across the sky above the ruined warden’s house
WIRED’s “Free Chinese AI Models Are Getting Scary Good at Hacking” Rests on Two Cyber Ranges: DeepSeek Is Level With the Leader on One, the GLM It Names Finished Last of Seven, and Three Claude Runs Stopped by Refusals Appear Only in the Chart Data
November 8, 2026
Stepped concrete headquarters building of Crédit Mutuel Arkéa at Le Relecq-Kerhuon in Brittany, seen across a lawn with bare trees and flowering shrubs
Crédit Mutuel Arkéa Says Attackers Reached Its Own Account Through a Service Provider’s Platform; the On-Chain Trail Attributed to the Theft Shows $126 Million in USDC Reaching 17 Wallets in 28 Hours, Most of It Moving On Within Minutes
November 8, 2026
SXZ.io SXZ.io
  • Home

Categories

Articles 290 Posts
News 294 Posts
Learning Hub 263 Posts
Home/Learning Hub/How to Fix Python ModuleNotFoundError and Relative Import Errors, Break Circular Imports, and Stop Local Files From Shadowing the Standard Library
Learning Hub

How to Fix Python ModuleNotFoundError and Relative Import Errors, Break Circular Imports, and Stop Local Files From Shadowing the Standard Library

Reproduce ModuleNotFoundError, relative-import and circular-import errors in a small project, learn what sys.path[0] is, fix them with python -m and a src layout, and catch files that hide...

November 8, 2026 27 Min Read
7

You run a Python file you just wrote and the terminal answers with ModuleNotFoundError: No module named 'shop' or ImportError: attempted relative import with no known parent package. Nothing is wrong with the code itself. Python is telling you that it looked for a module in a list of folders and did not find it, and that the list is not the one you assumed. In this tutorial we reproduce each of the classic import errors in a tiny project, learn to read what Python is telling us, and fix every one of them properly instead of with a hack.

Table Of Content

  • Words we will use
  • Prerequisites
  • The idea: Python searches an ordered list of folders, and the first match wins
  • Step 1: Create a small project to break
  • Step 2: Meet ModuleNotFoundError and learn what sys.path[0] is
  • Step 3: Fix it by starting the file as part of its package with -m
  • Step 4: Choose between absolute and relative imports
  • Step 5: Give the project a proper home on sys.path with a src layout and an editable install
  • 5a. Why do the tests pass with one command and fail with another?
  • 5b. Move the code under src and describe the project in pyproject.toml
  • 5c. Run the tests before installing, and expect them to fail
  • 5d. Install the project in editable mode
  • 5e. Check what you would actually ship
  • Step 6: Break and fix a circular import
  • Step 7: Stop local files from shadowing the standard library
  • 7a. A file named after the module it imports
  • 7b. Why the first entry is called “unsafe”
  • 7c. Let a script check for shadowing
  • 7d. A safe name today can become a standard-library name tomorrow
  • Step 8: A debugging checklist for any import error
  • Verify the whole thing end to end
  • Common mistakes
  • Where to go next
  • Sources

We also cover three things that most import tutorials skip. First, how you start Python (as a file, with -m, or with -c) changes the first folder it searches. Second, a project can pass its tests from your checkout and still be broken once installed, and a src layout with an editable install closes that gap. Third, a file named random.py or json.py next to your script silently replaces the standard library. That is a very common beginner bug, and it is also a way for anyone who can drop a file into a folder to run code inside your Python process. We finish with a short checker script, a test suite for it, and a Ruff rule that catch the problem before it bites.

Everything here runs in a scratch folder on your own machine. The “planted” file in Step 7 only prints one sentence.

Words we will use

  • A module is one Python file (or a built-in) that you can import, such as json or shop/cart.py.
  • A package is a folder of modules that Python treats as one importable unit. A regular package has a file named __init__.py inside it, and that file may be empty.
  • sys.path is the ordered list of folders Python searches when you import something. sys.modules is Python’s cache of the modules it has already loaded.
  • An absolute import names a module from the top, as in from shop.pricing import total. A relative import names it from the current file’s package using dots, as in from .pricing import total.
  • An editable install tells Python to use your working copy of a project instead of a copied snapshot, so edits take effect immediately.
  • The standard library is the set of modules that ships with Python, such as json, random and pathlib.

Prerequisites

  • Python 3.11 or newer. The -P option used in Step 7 was added in 3.11. The lab ran on Python 3.13.14 (64-bit, Windows), with pip 26.1.2, pytest 9.2.0 and Ruff 0.17.4. The last experiment in Step 7 also uses python.org’s 3.15.2 embeddable build, which you can skip.
  • Windows 11 is where I ran every command. The commands are the same in PowerShell, cmd.exe, macOS and Linux shells except where a note says otherwise (virtual environment activation, Scripts versus bin, and path separators). I did not run the macOS and Linux variants on this machine.
  • A terminal, a text editor, and an internet connection for installing pytest and Ruff.
  • No packaging knowledge. If you can run a Python file and read a traceback, you have what you need.

Create a scratch folder and open a terminal in it. In the outputs below the project appears as C:\work\demo (I shortened my real path to keep the output readable); your outputs will show your own folder.

The idea: Python searches an ordered list of folders, and the first match wins

When a program runs import shop.cart, Python checks three places in order. It first looks in sys.modules; the import reference calls this “the first place checked during import search” and describes it as a cache of every module imported so far. It then checks the built-in modules compiled into the interpreter. Finally it walks the folders in sys.path from first to last and stops at the first folder that contains a match. That last rule is why the order of the list matters so much.

import shop.cart
  1. sys.modules          already imported? reuse it
  2. built-in modules     compiled into the interpreter
  3. sys.path, in order   first match wins
       [0]  see the table below
            then PYTHONPATH, the standard library, and site-packages

Python builds sys.path when it starts. The Python tutorial says it is initialized from the folder containing the input script (or the current folder when no file is given), then the folders in the PYTHONPATH environment variable, then an installation-dependent default that, by convention, includes a site-packages folder (the standard library is on the list too, as Step 2 will show). The first entry causes most beginner errors, because it depends on how you started Python.

How you start Python sys.path[0] is
python shop/cli.py the folder that contains the script (here shop)
python -m shop.cli the current folder
python -c "..." or the interactive prompt an empty string, which means the current folder

That table comes from the sys.path documentation, which calls this first entry “a potentially unsafe path” and says that the -P option or the PYTHONSAFEPATH environment variable stops it being added. We will see why it earns that name in Step 7. The tutorial also warns that the script’s folder “is placed at the beginning of the search path, ahead of the standard library path”, which means “scripts in that directory will be loaded instead of modules of the same name in the library directory.”

Step 1: Create a small project to break

Inside the scratch folder, create this layout. The empty __init__.py marks shop as a regular package; leave it empty for now. Each file below starts with a comment that names its path, so you know where it goes.

demo/
  shop/
    __init__.py      (empty file)
    pricing.py
    cart.py
    cli.py
    probe.py
  tests/
    test_cart.py

The first file does the arithmetic and knows nothing about imports.

# shop/pricing.py
def total(prices):
    """Add up a list of prices."""
    return sum(prices)

The cart module imports total from pricing with an absolute import: it spells out the full path from the top-level package, shop.

# shop/cart.py
from shop.pricing import total


def checkout(prices):
    return f"total: {total(prices):.2f}"

The command line module imports checkout and prints a result. The if __name__ == "__main__": guard runs main() only when this file is the program you started, not when another module imports it.

# shop/cli.py
from shop.cart import checkout


def main():
    print(checkout([4.50, 3.25, 12.00]))


if __name__ == "__main__":
    main()

The probe prints three facts we need in a minute: the first entry of sys.path, the module’s __name__, and its __package__.

# shop/probe.py
import sys

print("sys.path[0] =", repr(sys.path[0]))
print("__name__    =", repr(__name__))
print("__package__ =", repr(__package__))

Finally, a test. It imports from the package the same way real code would.

# tests/test_cart.py
from shop.cart import checkout


def test_checkout_formats_the_total():
    assert checkout([1.50, 2.50]) == "total: 4.00"

Step 2: Meet ModuleNotFoundError and learn what sys.path[0] is

From the demo folder, run the command line module the way most beginners do: by pointing Python at the file.

python shop/cli.py
Traceback (most recent call last):
  File "C:\work\demo\shop\cli.py", line 2, in <module>
    from shop.cart import checkout
ModuleNotFoundError: No module named 'shop'

The traceback is short and honest. Line 2 of cli.py asked for a module named shop, and no folder on sys.path had one. But shop is right there in demo. To see why Python did not find it, run the probe three different ways.

python shop/probe.py
sys.path[0] = 'C:\\work\\demo\\shop'
__name__    = '__main__'
__package__ = None
python -m shop.probe
sys.path[0] = 'C:\\work\\demo'
__name__    = '__main__'
__package__ = 'shop'
python -c "import sys; print(repr(sys.path[0]))"
''

Read the three results side by side. Started as a script, Python put the script’s own folder, demo\shop, first, and that folder has no shop inside it. The package lives one level up, in demo, which was never searched. Started with -m, Python put the current folder first, which is demo, so shop is found. Started with -c, the entry is an empty string, which the documentation defines as the current folder. The __package__ line explains the other classic error, which we reach in Step 4: started as a script, the file belongs to no package (None), while under -m it knows it lives in shop.

If you want to see the whole list, print it. Yours will differ from mine, but the shape is the same: the first entry from the table above, then the standard library, then the folders where installed packages live.

python -c "import sys; print(*sys.path, sep='\n')"

C:\Python313\python313.zip
C:\Python313\DLLs
C:\Python313\Lib
C:\Python313
C:\Users\you\AppData\Roaming\Python\Python313\site-packages
C:\Python313\Lib\site-packages

The blank first line is the empty-string entry from the -c start.

Step 3: Fix it by starting the file as part of its package with -m

Give Python the module name instead of the file name, and run the command from the project root:

python -m shop.cli
total: 19.75

It works because -m puts the current folder first on sys.path. The command-line documentation says so in one sentence: “As with the -c option, the current directory will be added to the start of sys.path.” Two details trip people up. The module name uses dots and no .py (shop.cli, not shop/cli.py), and you must run it from the project root. From inside shop it fails, with a message that now makes sense:

cd shop
python -m shop.cli
cd ..
C:\Python313\python.exe: Error while finding module specification for 'shop.cli' (ModuleNotFoundError: No module named 'shop')

The current folder was shop, so Python looked for shop inside shop and found nothing.

You will also read advice to set the PYTHONPATH environment variable to the project folder. It does work: with PYTHONPATH set to the demo folder, the original python shop/cli.py command prints the right answer.

total: 19.75

Treat that as a stopgap. The setting lives in your shell, your editor or your CI configuration instead of in the project, so a teammate who has not set it gets the error back. Step 5 gives the project a proper home on the path.

Step 4: Choose between absolute and relative imports

So far cart.py and cli.py use absolute imports. Python also supports relative imports, which name a module by its position next to the current file. The import reference puts it this way: “Relative imports use leading dots. A single leading dot indicates a relative import, starting with the current package. Two or more leading dots indicate a relative import to the parent(s) of the current package, one level per dot after the first.” The same page adds the rule that explains why you never see import .pricing: “relative imports may only use the second form”, meaning the from X import Y form.

Change the two files to use relative imports and keep everything else the same.

# shop/cart.py  (relative import)
from .pricing import total


def checkout(prices):
    return f"total: {total(prices):.2f}"
# shop/cli.py  (relative import)
from .cart import checkout


def main():
    print(checkout([4.50, 3.25, 12.00]))


if __name__ == "__main__":
    main()

Now start the command line module as a script again:

python shop/cli.py
Traceback (most recent call last):
  File "C:\work\demo\shop\cli.py", line 2, in <module>
    from .cart import checkout
ImportError: attempted relative import with no known parent package

This is the second classic error. The probe in Step 2 already showed the cause: started as a script, a file has __package__ set to None, so a leading dot has no package to be relative to. The import reference makes the same point about the __main__ module: its spec is None when you run a file directly, and “Use the -m switch if valid module metadata is desired in __main__.” Start it with -m and Python knows the package, so the relative imports work:

python -m shop.cli
total: 19.75

Too many dots has its own error. Add a module that reaches one package higher than shop:

# shop/oops.py
from .. import config
python -c "import shop.oops"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
    import shop.oops
  File "C:\work\demo\shop\oops.py", line 2, in <module>
    from .. import config
ImportError: attempted relative import beyond top-level package

One dot means the current package, which is shop. Two dots mean the parent of shop, and shop is already the top of the tree, so there is nothing to climb to. Delete oops.py again.

Which style should you use? PEP 8 says: “Absolute imports are recommended, as they are usually more readable and tend to be better behaved (or at least give better error messages) if the import system is incorrectly configured”. It then allows the other style: “explicit relative imports are an acceptable alternative to absolute imports, especially when dealing with complex package layouts where using absolute imports would be unnecessarily verbose”. We will follow the default. Put the absolute versions of cart.py and cli.py from Step 1 back, and confirm that they still run:

python -m shop.cli
total: 19.75

One more trap before we move on. It is tempting to make from shop import main work by importing it in the package’s __init__.py:

# shop/__init__.py  (re-exports main for convenience)
from shop.cli import main

Now start the module with -m again:

python -m shop.cli
<frozen runpy>:130: RuntimeWarning: 'shop.cli' found in sys.modules after import of package 'shop', but prior to execution of 'shop.cli'; this may result in unpredictable behaviour
total: 19.75

The warning comes from runpy, the module behind -m. Python imports the package shop first, which runs __init__.py, which imports shop.cli under its real name; then -m wants to run the same file a second time as __main__. The import reference notes that a module run this way and the same module imported normally are “still considered distinct modules”, because blocks guarded by if __name__ == "__main__": “only execute when the module is used to populate the __main__ namespace, and not during normal import.” The fix is to keep __init__.py light: empty it again, and import the submodules you need where you use them.

Step 5: Give the project a proper home on sys.path with a src layout and an editable install

Every fix so far depends on the folder you are standing in. Real projects need something sturdier, and the test runner is where the weakness shows first.

5a. Why do the tests pass with one command and fail with another?

Create a virtual environment inside the project, activate it, and install pytest. A virtual environment is a private copy of Python’s package area, so nothing you install touches your system Python. The venv documentation lists the activation command for each shell: .venv\Scripts\Activate.ps1 in PowerShell, .venv\Scripts\activate.bat in cmd.exe, and source .venv/bin/activate in bash or zsh. It also says that “You don’t specifically need to activate a virtual environment, as you can just specify the full path to that environment’s Python interpreter when invoking Python”, which is how my lab ran every step.

python -m venv .venv
.venv\Scripts\Activate.ps1
python -m pip install pytest

I install with python -m pip so that pip runs as a module of the interpreter I typed. The package then lands in the same Python that will import it, and the pip documentation writes its commands that way too (python -m pip <command> [options]). Now run the test suite with the pytest command:

pytest -q
_____________________ ERROR collecting tests/test_cart.py _____________________
tests\test_cart.py:2: in <module>
E   ModuleNotFoundError: No module named 'shop'
!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.01s

pytest prints more than this; I trimmed the output to the lines that matter. The test file cannot import shop. Run the same tests through Python instead:

python -m pytest -q
1 passed in 0.01s

Same tests, same folder, different result. The pytest documentation explains the difference: running through python -m is almost equivalent to the pytest command, “except that calling via python will also add the current directory to sys.path.” Without that, pytest’s default import mode only inserts the test file’s own folder, and the pytest import-mode documentation says “The directory path containing each module will be inserted into the beginning of sys.path if not already there.” That folder is tests, and shop is not in it.

There are two more quick fixes you will meet. One is a pytest.ini file in the project root that tells pytest which folders to add, using the pythonpath setting (“Directories will be added to the head of sys.path”, per the pytest reference):

[pytest]
pythonpath = .
pytest -q
1 passed in 0.01s

The other is an empty tests/__init__.py. In my run, that also turned the bare pytest command green (delete the pytest.ini first if you want to see it work alone):

pytest -q
1 passed in 0.01s

My reading of why: with an __init__.py, pytest gives the test module its full name tests.test_cart, which means it has to put the folder above tests on sys.path, and shop happens to live in that folder.

All three tricks work, and that is the problem. Each puts your working folder on the path, so the tests import the copy of shop that sits in your checkout. They would pass even if the project could not be installed at all. The Python Packaging User Guide describes the trap: “The src layout helps prevent accidental usage of the in-development copy of the code. This is relevant since the Python interpreter includes the current working directory as the first item on the import path.” (src layout vs flat layout)

5b. Move the code under src and describe the project in pyproject.toml

Delete pytest.ini and tests/__init__.py if you created them. Make a folder named src next to tests and move the whole shop folder into it. Add a __main__.py to the package (it makes python -m shop work) and a pyproject.toml in the project root.

demo/
  pyproject.toml
  src/
    shop/
      __init__.py
      __main__.py
      pricing.py
      cart.py
      cli.py
      probe.py
  tests/
    test_cart.py
# src/shop/__main__.py
from shop.cli import main

main()
# pyproject.toml
[build-system]
requires = ["setuptools>=64"]
build-backend = "setuptools.build_meta"

[project]
name = "shop-demo"
version = "0.1.0"
requires-python = ">=3.9"

[project.scripts]
shop = "shop.cli:main"

The three tables each do one job. [build-system] tells pip which tool turns this folder into an installable package. I chose setuptools 64 or newer because the setuptools documentation notes “Changed in version v64.0.0: Editable installation hooks implemented according to PEP 660.” [project] names the project and states the oldest Python you support. [project.scripts] creates a command: the packaging guide says “To install a command as part of your package, declare it in the [project.scripts] table”, and executing it “will do the equivalent of import sys; from spam import main_cli; sys.exit(main_cli())”. We never told setuptools where the package is, because with a src folder it finds it by itself. Its discovery documentation says this layout is “very handy when you wish to use automatic discovery, since you don’t have to worry about other Python files or folders in your project root being distributed by mistake.”

5c. Run the tests before installing, and expect them to fail

python -m pytest -q
_____________________ ERROR collecting tests/test_cart.py _____________________
tests\test_cart.py:2: in <module>
E   ModuleNotFoundError: No module named 'shop'
!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.01s

This failure is the whole point. The current folder no longer contains a shop package, so nothing can hide the fact that the project is not installed yet. Even python -m pytest cannot rescue it.

5d. Install the project in editable mode

python -m pip install -e .

The -e flag means editable, and the dot means the project in this folder. pip reads [build-system], builds an editable version of the project, and installs it. The command ends with one line:

Successfully installed shop-demo-0.1.0

Run the tests both ways:

python -m pytest -q
1 passed in 0.01s
pytest -q
1 passed in 0.01s

Both pass, because shop is now installed rather than found by accident. To see how, ask pip, and then look inside the environment’s package folder:

python -m pip show shop-demo
Name: shop-demo
Version: 0.1.0
Location: C:\work\demo\.venv\Lib\site-packages
Editable project location: C:\work\demo
Get-Content .venv\Lib\site-packages\__editable__.shop_demo-0.1.0.pth
C:\work\demo\src

That command is PowerShell; on macOS and Linux the folder is .venv/lib/python3.13/site-packages and you can read the file with cat, which I did not run here. The install dropped a one-line path file into site-packages that points at src. The setuptools documentation describes the mechanism: “Under the hood, setuptools will try to create a special .pth file in the target directory (usually site-packages) that extends the PYTHONPATH or install a custom import hook.” The sys.path documentation points to the site module for “how to use .pth files to extend sys.path.” In my run setuptools chose the simple route, one path file, so Python adds src to sys.path at startup. Our code in src is importable from anywhere, and edits are live. (Path files are read each time Python starts, which is why they are worth auditing.)

With the environment still active, step out of the project folder and prove it:

cd ..
python -c "import shop; print(shop.__file__)"
C:\work\demo\src\shop\__init__.py

pip also installed a launcher for the command we declared. On Windows it is shop.exe in the environment’s Scripts folder (bin on macOS and Linux), and with the environment active you can type its name. The __main__.py file gives you the module form too:

shop
total: 19.75
python -m shop
total: 19.75

5e. Check what you would actually ship

An editable install points at your working folder, so it cannot tell you what a user would receive. Go back into the project, build a wheel (the file pip installs from an index), and list what is inside:

cd demo
python -m pip wheel . --no-deps -w dist
python -c "import zipfile; print(*zipfile.ZipFile('dist/shop_demo-0.1.0-py3-none-any.whl').namelist(), sep='\n')"
Successfully built shop-demo
shop/__init__.py
shop/__main__.py
shop/cart.py
shop/cli.py
shop/pricing.py
shop/probe.py
shop_demo-0.1.0.dist-info/METADATA
shop_demo-0.1.0.dist-info/WHEEL
shop_demo-0.1.0.dist-info/entry_points.txt
shop_demo-0.1.0.dist-info/top_level.txt
shop_demo-0.1.0.dist-info/RECORD

Everything under src/shop ships, including probe.py; delete it if you do not want to publish it. Now install that wheel into a brand-new environment next to demo and run the program from there:

cd ..
python -m venv fresh
fresh\Scripts\python -m pip install demo\dist\shop_demo-0.1.0-py3-none-any.whl
fresh\Scripts\shop
fresh\Scripts\python -c "import shop; print(shop.__file__)"
Successfully installed shop-demo-0.1.0
total: 19.75
C:\work\fresh\Lib\site-packages\shop\__init__.py

The program runs from an installed copy that has no connection to your working folder; shop.__file__ now points into the new environment’s site-packages. This is the check a src layout makes cheap. The packaging guide describes the workflow: “typically, an editable installation is used for development and a regular installation is used for testing”. Return to the project folder with cd demo before the next step.

Step 6: Break and fix a circular import

Imports can also fail inside a package. Add two modules that need each other: a customer has orders, and an order belongs to a customer. Each one imports the other so it can name the other’s class in type annotations.

# src/shop/customer.py
from shop.order import Order


class Customer:
    def __init__(self, name: str) -> None:
        self.name = name
        self.orders: list[Order] = []

    def add_order(self, order: Order) -> None:
        self.orders.append(order)
# src/shop/order.py
from shop.customer import Customer


class Order:
    def __init__(self, customer: Customer, total: float) -> None:
        self.customer = customer
        self.total = total
        customer.add_order(self)
python -c "import shop.order"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
    import shop.order
  File "C:\work\demo\src\shop\order.py", line 2, in <module>
    from shop.customer import Customer
  File "C:\work\demo\src\shop\customer.py", line 2, in <module>
    from shop.order import Order
ImportError: cannot import name 'Order' from partially initialized module 'shop.order' (most likely due to a circular import) (C:\work\demo\src\shop\order.py)

Read the traceback from the top. Importing shop.order starts running order.py. Its line 2 asks for shop.customer, so Python starts running customer.py. Line 2 of that file asks for Order from shop.order, but shop.order is only half finished: it is paused on its own line 2, before class Order exists, so the name is not there yet. Python calls such a module “partially initialized” and names the likely cause in parentheses. Importing the other module first fails too, with the roles swapped:

python -c "import shop.customer"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
    import shop.customer
  File "C:\work\demo\src\shop\customer.py", line 2, in <module>
    from shop.order import Order
  File "C:\work\demo\src\shop\order.py", line 2, in <module>
    from shop.customer import Customer
ImportError: cannot import name 'Customer' from partially initialized module 'shop.customer' (most likely due to a circular import) (C:\work\demo\src\shop\customer.py)

The most common cycle is the one in this example: an import that exists only to name a type in an annotation. For that case two tools remove the run-time import. The first is from __future__ import annotations, which makes Python store annotations as text instead of evaluating them when the function is defined. Our project declares Python 3.9 and newer, where that line is what keeps the annotation from being evaluated. The second is typing.TYPE_CHECKING, which the typing documentation calls “A special constant that is assumed to be True by static type checkers. It’s False at runtime.” We can check the run-time half ourselves:

python -c "import typing; print(typing.TYPE_CHECKING)"
False

An import placed under if TYPE_CHECKING: is therefore skipped when the program runs, while your editor and type checker still read it. Rewrite order.py like this. The helper at the bottom shows the second tool: when a function needs the real class at run time, import it inside that function (a late import), by which point both modules have finished loading.

# src/shop/order.py  (fixed)
from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from shop.customer import Customer


class Order:
    def __init__(self, customer: Customer, total: float) -> None:
        self.customer = customer
        self.total = total
        customer.add_order(self)


def order_for(name: str, total: float) -> Order:
    from shop.customer import Customer  # late import: this function needs the real class

    return Order(Customer(name), total)
python -c "import shop.order; print('imported shop.order')"
python -c "import shop.customer; print('imported shop.customer')"
python -c "from shop.order import order_for; o = order_for('Ada', 9.5); print(o.customer.name, o.total, len(o.customer.orders))"
imported shop.order
imported shop.customer
Ada 9.5 1

Both import orders work, and the helper builds a customer and an order that point at each other. If neither trick feels natural, the cycle is usually telling you that the two modules share a concept that belongs in a third module both can import. Moving the shared class or function there makes the dependency run in one direction only.

Step 7: Stop local files from shadowing the standard library

7a. A file named after the module it imports

Make a folder called dice inside the project and add a file named random.py. It looks harmless: a tiny dice roller that wants Python’s random module.

# dice/random.py
import random

random.seed(2026)
print("You rolled a", random.randint(1, 6))
cd dice
python random.py
Traceback (most recent call last):
  File "C:\work\demo\dice\random.py", line 2, in <module>
    import random
  File "C:\work\demo\dice\random.py", line 4, in <module>
    random.seed(2026)
    ^^^^^^^^^^^
AttributeError: module 'random' has no attribute 'seed' (consider renaming 'C:\work\demo\dice\random.py' since it has the same name as the standard library module named 'random' and prevents importing that standard library module)

Python 3.13.14 ends the message with a hint, and the hint is right. The script’s folder is first on sys.path, so the import random on line 2 found dice\random.py, which is the file itself, before it ever reached the standard library. Line 4 then asked that half-loaded module for seed, which it does not have. The traceback names the file twice because the program imported itself. To see which file a name resolves to without running it, ask the import system:

python -c "import importlib.util as u; print(u.find_spec('random').origin)"
C:\work\demo\dice\random.py

The real fix is to rename the file (roll.py would do). If you cannot rename it yet, start Python with -P, which we meet properly in a moment:

python -P random.py
You rolled a 1

7b. Why the first entry is called “unsafe”

Accidents are one thing. The same rule also means that anyone who can put a file into the folder you run Python from decides what your imports mean. Make a folder reports next to dice with two files: a legitimate script, and a file named json.py that stands in for a planted one. It only prints a sentence, but code in its place could do anything your account can do.

# reports/report.py
import json

print(json.dumps({"status": "ok"}))
# reports/json.py  (a stand-in for a planted file)
print("planted json.py is running with your permissions")


def dumps(obj):
    return "{}"
cd ..\reports
python report.py
planted json.py is running with your permissions
{}

The script asked for the standard library’s json and got its neighbour. The planted code ran first, with your permissions, and then the program printed {} instead of {"status": "ok"}, so the output was quietly wrong as well. Starting Python with -c or -m does the same, because both put the current folder first:

python -c "import json; print(json.__file__)"
planted json.py is running with your permissions
C:\work\demo\reports\json.py
python -m json.tool --help
planted json.py is running with your permissions
C:\work\demo\.venv\Scripts\python.exe: Error while finding module specification for 'json.tool' (ModuleNotFoundError: __path__ attribute not found on 'json' while trying to find 'json.tool')

Even a standard-library tool started with -m imported the neighbour first; the error afterwards is Python noticing that this json is not a package. Three switches stop the current or script folder from being searched first. The command-line documentation says -P means “Don’t prepend a potentially unsafe path to sys.path”, names the PYTHONSAFEPATH environment variable as its twin, and marks -P “Added in version 3.11”. It describes -I (isolated mode) as implying -E, -P and -s: “In isolated mode sys.path contains neither the script’s directory nor the user’s site-packages directory. All PYTHON* environment variables are ignored, too.” All three give the correct answer here:

python -P report.py
{"status": "ok"}
$env:PYTHONSAFEPATH = "1"
python report.py
Remove-Item Env:PYTHONSAFEPATH
{"status": "ok"}

That is PowerShell syntax; in bash the same thing is PYTHONSAFEPATH=1 python report.py, which I did not run in a Unix shell. I set the variable through the process environment in the lab.

python -I report.py
{"status": "ok"}
python -P -c "import json; print(json.__file__)"
C:\Python313\Lib\json\__init__.py

None of these turns Python into a sandbox; they only change which folders are searched first. The better habit is to run unfamiliar code from a folder you control, and to treat a freshly unzipped or cloned project folder as untrusted until you have looked at it. Go back to the project root with cd ...

7c. Let a script check for shadowing

Rules are easy to forget, so let a script do the remembering. Python publishes the names it cares about: sys.stdlib_module_names is, in the documentation’s words, “A frozenset of strings containing the names of standard library modules.” Two caveats from the same page shape our checker: “Test modules are excluded”, and “For packages, only the main package is listed”. Our script walks the project and reports every .py file or package folder whose name is on the list. Save it as shadowcheck.py in the project root.

# shadowcheck.py
import sys
from pathlib import Path

SKIP = {".git", ".venv", "venv", "env", "node_modules", "__pycache__", "build", "dist", ".tox"}


def find_shadows(root, names=None):
    """Return (relative path, module name) for project files that would hide a standard-library module."""
    names = sys.stdlib_module_names if names is None else names
    root = Path(root)
    hits = []
    for path in sorted(root.rglob("*")):
        parts = path.relative_to(root).parts
        if any(part in SKIP or part.endswith(".egg-info") for part in parts):
            continue
        if path.is_file() and path.suffix == ".py":
            name = path.stem
        elif path.is_dir() and (path / "__init__.py").exists():
            name = path.name  # only a folder with __init__.py can win over the standard library
        else:
            continue
        if name.startswith("__") and name.endswith("__"):
            continue  # __init__.py and __main__.py are normal
        if name in names:
            hits.append((path.relative_to(root), name))
    return hits


def main(argv):
    root = argv[1] if len(argv) > 1 else "."
    hits = find_shadows(root)
    for rel, name in hits:
        print(f"{rel.as_posix()} hides the standard-library module {name!r}")
    if not hits:
        print("no standard-library names are shadowed")
    return 1 if hits else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv))

One rule in that code needs a defence: a folder only counts if it contains an __init__.py. We prove it a little further down. For now, run the checker from the project root (it exits with status 1 when it finds something, so a CI job can fail on it):

python shadowcheck.py .
dice/random.py hides the standard-library module 'random'
reports/json.py hides the standard-library module 'json'

It found both planted files and ignored the virtual environment. Now give the checker its own tests, so the rules above cannot rot. Save this as test_shadowcheck.py next to it.

# test_shadowcheck.py
from shadowcheck import find_shadows


def names(root):
    return [(rel.as_posix(), name) for rel, name in find_shadows(root)]


def test_flags_a_file_named_like_a_stdlib_module(tmp_path):
    (tmp_path / "random.py").write_text("x = 1\n")
    assert names(tmp_path) == [("random.py", "random")]


def test_flags_a_regular_package_but_not_a_plain_folder(tmp_path):
    (tmp_path / "email").mkdir()
    (tmp_path / "email" / "__init__.py").write_text("")
    (tmp_path / "types").mkdir()  # no __init__.py, so it cannot hide the real types module
    assert names(tmp_path) == [("email", "email")]


def test_flags_nested_files_too(tmp_path):
    (tmp_path / "app").mkdir()
    (tmp_path / "app" / "json.py").write_text("")
    assert names(tmp_path) == [("app/json.py", "json")]


def test_ignores_normal_names_dunder_files_and_virtualenvs(tmp_path):
    (tmp_path / "cart.py").write_text("")
    (tmp_path / "__main__.py").write_text("")
    (tmp_path / ".venv" / "Lib").mkdir(parents=True)
    (tmp_path / ".venv" / "Lib" / "json.py").write_text("")
    assert names(tmp_path) == []


def test_uses_the_name_list_you_pass_in(tmp_path):
    (tmp_path / "profiling.py").write_text("")
    assert names(tmp_path) == []
    assert find_shadows(tmp_path, names={"profiling"})[0][1] == "profiling"
python -m pytest -q test_shadowcheck.py
5 passed in 0.01s

Linters can help too. Ruff has a rule for exactly this, A005 (stdlib-module-shadowing, added in Ruff 0.9.0). Its documentation describes the default: “By default, the module path relative to the project root or src directories is considered, so a top-level logging.py or logging/__init__.py will clash with the builtin logging module, but utils/logging.py, for example, will not.” That default misses our two files, because each sits one folder down from the root, even though the script-folder rule can still bite them. Install Ruff and try both settings:

python -m pip install ruff
python -m ruff check --select A005 --output-format concise .
All checks passed!

The documentation adds that with strict-checking set to true, “only the last component of the module name is considered, so logging.py, utils/logging.py, and utils/logging/__init__.py will all trigger the rule.” Turn that on from the command line:

python -m ruff check --select A005 --output-format concise --config "lint.flake8-builtins.strict-checking = true" .
dice\random.py:1:1: A005 Module `random` shadows a Python standard-library module
reports\json.py:1:1: A005 Module `json` shadows a Python standard-library module
Found 2 errors.

For a real project, put strict-checking = true under [tool.ruff.lint.flake8-builtins] in pyproject.toml instead of typing the flag each time.

Now the proof of the folder rule. PEP 420 explains it: while looking for a module or package, Python scans each folder on the path, and when it finds a plain folder with the right name but no __init__.py “it is recorded and the scan continues with the next directory in the parent path.” A regular package or a module found later in the list still wins over it. Try it: create a folder ns in the project root, and inside it an empty folder named random.

cd ns
python -c "import random; print(random.__file__)"
C:\Python313\Lib\random.py

The standard library’s random won. Now add an empty __init__.py to ns\random and run the same command again:

C:\work\demo\ns\random\__init__.py

With the marker file, the local folder wins. Go back to the project root with cd ... We will use the ns folder once more in Step 8, so keep it for now.

7d. A safe name today can become a standard-library name tomorrow

The documentation says the compression package was “Added in version 3.14” and profiling “Added in version 3.15”. Both are plausible names for a file in your own project. Make a folder upgrade with two empty files named compression.py and profiling.py, then run the checker with two interpreters: the Python 3.13 we have used all along, and python.org’s 3.15.2 embeddable build unzipped to C:\py315.

python shadowcheck.py upgrade
no standard-library names are shadowed
C:\py315\python.exe shadowcheck.py upgrade
compression.py hides the standard-library module 'compression'
profiling.py hides the standard-library module 'profiling'

The same project is clean on one version and has two collisions on the other. Run the checker with the newest Python you plan to support, because new releases add names. Delete upgrade when you are done.

Step 8: A debugging checklist for any import error

When an import fails, answer three questions in order: which Python is running, where did it look, and what did it find? These commands answer them without guessing. Run them with the project’s environment active.

python -c "import sys, shop; print(sys.executable); print(shop.__file__)"
C:\work\demo\.venv\Scripts\python.exe
C:\work\demo\src\shop\__init__.py

sys.executable is the interpreter that is running, and shop.__file__ is the file that satisfied the import, which is how you notice a stale install or a shadowing file. To locate a module without running it, use find_spec as in Step 7:

python -c "import importlib.util as u; print(u.find_spec('shop.cart').origin)"
C:\work\demo\src\shop\cart.py

A ModuleNotFoundError for a third-party package is very often an interpreter mismatch: the package was installed into one Python and your program runs in another. In this fresh environment, yaml was never installed, so asking for it fails:

python -c "import yaml"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
    import yaml
ModuleNotFoundError: No module named 'yaml'

Print sys.executable from the program that fails (your editor may run a different Python than your terminal does), then install with python -m pip install using that same interpreter. Finally, if __file__ is None, the name resolved to a namespace package, which is a folder with no __init__.py. PEP 420 says such a package “Does not have a __file__ attribute”; on Python 3.13 the attribute exists and is None. The ns folder from Step 7 is an example (create it again with an empty random folder inside if you deleted it):

python -c "import ns; print(ns.__file__); print(list(ns.__path__))"
None
['C:\\work\\demo\\ns']

This table maps the messages from this tutorial to their causes.

What Python printed What it means Usual fix
ModuleNotFoundError: No module named 'x' No folder on sys.path has x Check the interpreter, install with python -m pip, or start from the right folder with -m
Error while finding module specification for 'shop.cli' -m ran from the wrong folder Run it from the project root, or install the project
attempted relative import with no known parent package The file was started as a script, so it has no package Use python -m package.module, or an absolute import
attempted relative import beyond top-level package More dots than the package has levels Count the dots, or restructure
cannot import name 'X' from partially initialized module Two modules import each other TYPE_CHECKING, a late import, or move the shared code
module 'x' has no attribute 'y' (consider renaming ...) One of your files hides a standard-library module Rename the file and run shadowcheck.py
RuntimeWarning: 'a.b' found in sys.modules after import of package 'a' A package’s __init__.py imports a submodule you run with -m Keep __init__.py light

Verify the whole thing end to end

Delete the experiment folders (dice, reports, ns, upgrade), activate the project’s environment, and run these from the project root:

python -m pytest -q tests
shop
python shadowcheck.py .
1 passed in 0.01s
total: 19.75
no standard-library names are shadowed

The tests pass against the installed project, the shop command works from any folder, and no file in the project hides a standard-library module. Your timing in the pytest line will differ.

Common mistakes

  • Running a file inside a package as a script. python shop/cli.py makes the package’s own folder the first stop on the path and leaves __package__ empty. Use python -m shop.cli from the project root, or better, install the project and give it a command.
  • Installing into one Python and running another. A bare pip can belong to a different interpreter than the python you typed. python -m pip removes the doubt, and sys.executable tells you who is running.
  • Naming a file after a standard-library module. random.py, json.py, email.py, types.py and logging.py are all legal file names and all bad ideas. Run shadowcheck.py or Ruff’s A005 in strict mode.
  • Letting a green test run prove the project is installable. python -m pytest, PYTHONPATH, pythonpath = . and editor settings all put your working folder on the path. A src layout and an install into a fresh environment are much harder to fool.
  • Importing submodules in __init__.py for convenience. It invites circular imports and the -m double-import warning from Step 4.
  • Forgetting __init__.py in a folder you meant as a package. It still imports, as a namespace package with no __file__ (Step 8), which is rarely what you meant. Add the file, or choose a namespace package on purpose.

Where to go next

You now know where Python looks, why the first entry changes with how you start it, and how to give a project a stable home on the path. If import time is your problem rather than import errors, read our tutorial on Python 3.15 lazy imports. If you want to see what a hostile package does at import time, our tutorial on catching a malicious package with audit hooks builds on the same search rules. To go deeper on the pieces used here, the import system reference, the src layout discussion and the pytest import-mode explanation are the primary sources behind this post.

Sources

  • Python documentation: sys.path and sys.stdlib_module_names
  • Python documentation: command line and environment (-m, -c, -P, -I)
  • Python language reference: the import system
  • Python tutorial: modules and the module search path
  • Python documentation: venv and typing
  • PEP 8 and PEP 420
  • Python Packaging User Guide: writing pyproject.toml
  • setuptools: development mode and package discovery
  • pytest: usage and reference
  • Ruff: stdlib-module-shadowing (A005)
  • pip documentation

Tags:

Circular ImportsImport SystemModule ShadowingModuleNotFoundErrorPythonPython Packaging

Share

The Alcatraz lighthouse at night with beams of light sweeping across the sky above the ruined warden’s house
Previous Post

WIRED’s “Free Chinese AI Models Are Getting Scary Good at Hacking” Rests on Two Cyber Ranges: DeepSeek Is Level With the Leader on One, the GLM It Names Finished Last of Seven, and Three Claude Runs Stopped by Refusals Appear Only in the Chart Data

No Comment! Be the first one.

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Latest
08 Nov
How to Fix Python ModuleNotFoundError and Relative Import Errors, Break Circular Imports, and Stop Local Files From Shadowing the Standard Library
08 Nov
WIRED’s “Free Chinese AI Models Are Getting Scary Good at Hacking” Rests on Two Cyber Ranges: DeepSeek Is Level With the Leader on One, the GLM It Names Finished Last of Seven, and Three Claude Runs Stopped by Refusals Appear Only in the Chart Data
Trending
November 8, 2026
How to Fix Python ModuleNotFoundError and Relative Import Errors, Break Circular Imports, and Stop Local Files From Shadowing the Standard Library
November 8, 2026
WIRED’s “Free Chinese AI Models Are Getting Scary Good at Hacking” Rests on Two Cyber Ranges: DeepSeek Is Level With the Leader on One, the GLM It Names Finished Last of Seven, and Three Claude Runs Stopped by Refusals Appear Only in the Chart Data
November 8, 2026
Crédit Mutuel Arkéa Says Attackers Reached Its Own Account Through a Service Provider’s Platform; the On-Chain Trail Attributed to the Theft Shows $126 Million in USDC Reaching 17 Wallets in 28 Hours, Most of It Moving On Within Minutes
November 8, 2026
How to Stop Command Injection in Python subprocess Calls, Including Git Option Injection and the Windows .cmd Trap
November 8, 2026
Cloudflare’s Trusted Agent Delegation Essay Names 14 Site Problems, but the Identity Standard It Prefers Defines No Delegation, and Its First Shipped Fix Sees Agents Only at Login and Sign-Up
November 8, 2026
CloudLinux Says an Exploit Hit a CloudLinux 8 Server on November 4 Through Linux Kernel Bug CVE-2026-98276, Fixed Upstream Since September, and Red Hat Still Lists Every Supported RHEL Kernel as Affected

Related Posts

A laptop wrapped in a chain and padlock, illustrating least-privilege controls for AI agents.
Learning Hub

How to Secure Tool-Using AI Agents Before They Touch Production

June 8, 2026
Colorful sticky notes arranged on an office wall, symbolizing governance checklists and planning.
Learning Hub

AI Governance for Agentic Apps: A Practical Checklist for Builders

June 8, 2026
A technician connects green fiber optic cables at a data center, representing a private production inference endpoint.
Learning Hub

How to Deploy a Fine-Tuned LLM Behind a Private Production Inference Endpoint

June 8, 2026
Narrow aisle behind black supercomputer racks in a data center
Learning Hub

Kubernetes SELinux Volume Labeling: What Cluster Operators Should Audit Before v1.37

June 8, 2026
SXZ.io SXZ.io
  • [email protected]

Categories

Articles
Learning Hub
News

All Rights Reserved by SXZ.io ©2026