On This Page
1. The Error
Traceback (most recent call last):
File "/tmp/repro/pkg/main.py", line 1, in <module>
from . import utils
ImportError: attempted relative import with no known parent packageNo caret markers, no "Did you mean" hint, no __cause__/__context__ chain — this is a flat, single-frame ImportError raised the moment the interpreter tries to resolve from . import utils. The message text is identical on 3.9, 3.10, 3.11, 3.12, and 3.13; I ran the exact reproduction below on 3.10.20, 3.12.3, and 3.13.13 and got byte-for-byte the same traceback on all three. PEP 657's fine-grained error locations (3.11+) don't add anything here because the entire statement — not a sub-expression inside it — is what fails, so there's nothing narrower to point a caret at.
You'll also see the older, near-identical wording depending on how deep the relative import is:
ImportError: attempted relative import beyond top-level packageThat's the same root cause (Python can't place the current module inside a package hierarchy) tripped by a from .. or deeper dotted import instead of a single from ..
2. How to Reproduce It
Directory layout:
myproject/
└── pkg/
├── __init__.py
├── utils.py
└── main.py# pkg/utils.py
def helper():
return "helper result"# pkg/main.py
from . import utils
def run():
print(utils.helper())
if __name__ == "__main__":
run()# pkg/__init__.py
# (empty)Run it the way almost everyone does first — by pointing the interpreter straight at the file:
$ cd myproject
$ python pkg/main.py
Traceback (most recent call last):
File "/tmp/repro/pkg/main.py", line 1, in <module>
from . import utils
ImportError: attempted relative import with no known parent packageIt also fails identically if you cd pkg first and run python main.py from inside the package directory — the problem isn't your current working directory, it's how the file was launched.
Compare that to running it as a module with -m, from the project root:
$ python -m pkg.main
helper resultSame file, same import, no environment variables changed — only the invocation differs, and that's the entire bug.
3. Version Behaviour Matrix (Python 3.11 / 3.12 / 3.13 / 3.14 / 3.15)
This error is version-neutral — the mechanism (script execution has no parent package) hasn't changed since relative imports were introduced by PEP 328, and it won't change going forward. What has moved around it:
| Version | Behavior | Notes |
|---|---|---|
| 3.9 – 3.10 | Same error, same wording | 3.9 is EOL (2025-10); 3.10 is security-only, EOL 2026-10 per devguide.python.org/versions |
| 3.11 | Same error | Adds -P / PYTHONSAFEPATH (see below) — doesn't fix this, but changes a related failure mode |
| 3.12 | Same error | distutils removal (PEP 632) is unrelated but often hits the same "packaging layout" projects |
| 3.13 | Same error | Confirmed identical traceback in this article's repro |
| 3.14 (current bugfix, released 2025-10-07) | Same error | No change to import statement resolution for this case |
3.15 (feature-frozen; 3.15.0rc2 scheduled 2026-09-01, final 2026-10-01) | Same error | PEP 810 adds explicit lazy import syntax, but lazy imports still resolve relative imports through the same package-context machinery — a lazy from . import utils inside a script with no parent package fails the same way, just later (on first access instead of at the lazy import line) |
The one thing worth flagging for 3.11+: the -P flag and PYTHONSAFEPATH variable (added in 3.11) stop Python from prepending the current directory to sys.path for -c, the REPL, and — critically — for -m itself. That means python -P -m pkg.main run from outside the project root can turn this into ModuleNotFoundError: No module named 'pkg' instead, because pkg was never importable in the first place. -P is a security hardening flag, not a relative-import fix.
4. Why It Happens — Surface Level
Relative imports (from . import x, from .. import y) aren't resolved relative to a file's location on disk — they're resolved relative to the importing module's package. When you run python pkg/main.py, Python doesn't import pkg.main as a submodule of pkg; it executes main.py directly and calls the resulting module __main__. A module named __main__ has no package, so from . import utils has no package to be "relative to," and CPython refuses to guess.
5. Why It Happens — Under the Hood
Every module CPython runs carries two identity markers: __name__ and __package__ (backed by __spec__ when the module was found through the import system). Compare the two invocations from the reproduction:
$ python pkg/show_ctx.py
__name__ = __main__
__package__ = None
__spec__ = None
$ python -m pkg.show_ctx
__name__ = __main__
__package__ = pkg
__spec__ = ModuleSpec(name='pkg.show_ctx', loader=<_frozen_importlib_external.SourceFileLoader ...>, origin='/tmp/repro/pkg/show_ctx.py')Both runs report __name__ == "__main__" — that part is identical, which is why "check if __name__ == "__main__"" is not the fix people think it is here. The difference that actually matters is __package__ and __spec__.
python -m pkg.show_ctx goes through runpy, which uses importlib to find pkg.show_ctx as a real module inside the pkg package: it walks sys.meta_path, gets a PathFinder to build a ModuleSpec with name='pkg.show_ctx', and derives __package__ from that spec (spec.parent, which is pkg). python pkg/show_ctx.py skips all of that — the interpreter just compiles and executes the file's source directly as the top-level script, assigns it the synthetic name __main__, and there is no ModuleSpec at all, so __package__ ends up None.
When the bytecode for from . import utils runs, IMPORT_NAME calls __import__("utils", globals(), locals(), (), 1) — that trailing 1 is the relative-import level (one dot). The import system's job is then to resolve level 1 against globals()['__package__']. With __package__ is None (and no dots to walk in __name__, since it's just "__main__"), importlib._bootstrap._calc___package__ and the relative-import resolution in _find_and_load/_sanity_check have nothing to resolve against, so they raise ImportError: attempted relative import with no known parent package before ever consulting sys.path or sys.meta_path for utils itself. Note what this means: the error fires during package resolution, not during the search for utils.py — utils.py sitting right next to main.py on disk is irrelevant, because the relative import never gets far enough to look for it.
This is also why sys.path[0] matters for the fix even though it isn't the cause of the error. For python pkg/main.py, sys.path[0] is set to the script's own directory (.../myproject/pkg), so an absolute import utils would work — the error is specifically about the relative-import machinery, not about whether utils is reachable at all. For python -m pkg.main, sys.path[0] is instead the current working directory (myproject), which is what lets pkg itself be found as a package in the first place. You can see this directly:
$ python pkg/show_path.py
sys.path[0] = /tmp/repro/pkg
$ python -m pkg.show_path
sys.path[0] = /tmp/repro6. The Fix
Quick fix — run it as a module, not a script:
- python pkg/main.py
+ python -m pkg.mainRun from the project root (the directory containing pkg/), not from inside pkg/. This is a zero-code-change fix and is correct for throwaway scripts and quick local runs.
Correct fix for anything that ships — install it as a real package so there's no "run it right" convention to remember or explain in a README:
# pyproject.toml
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "pkgdemo"
version = "0.1.0"
requires-python = ">=3.10"
[project.scripts]
pkgdemo = "pkg.main:run"python -m venv .venv
source .venv/bin/activate
pip install -e .
pkgdemo # works from anywhere
python -m pkg.main # also works from anywhere nowI verified both entry points against the reproduction above — pip install -e . followed by the generated pkgdemo console script, and python -m pkg.main run from /tmp (outside the project entirely), both print helper result. Once the package is installed (even in editable mode), pkg is importable from any working directory, so __package__ resolves correctly no matter how the entry point is launched.
What doesn't fix it: adding sys.path.insert(0, "..") hacks, converting the relative import to try/except ImportError fallback imports, or wrapping the file in if __name__ == "__main__": — none of these change __package__, which is the actual value the import system needs.
7. Best Practices & The Better Design
The underlying anti-pattern is treating a package's internal module as something you run by path. Once a project has more than one file that imports from a sibling, stop running files directly and give the project a real entry point:
- Use a
src/layout (src/pkg/...) pluspip install -e ., so there is exactly one way to getpkgonsys.pathand it's the same in dev, CI, and production. - Expose behavior through
[project.scripts]console entry points instead of "run this specific file withpython." - If you need a script-like top level, put a thin
__main__.pyin the package (pkg/__main__.py) sopython -m pkgworks without needing to know the submodule name. - Never reach for
sys.path.insertorPYTHONPATH=.in application code as a substitute for installing the package — it papers over the exact mechanism described above and breaks again the moment someone runs the file a different way (a different CWD, a different IDE run configuration, a test runner).
# pkg/__main__.py
from .main import run
if __name__ == "__main__":
run()python -m pkg # now works too8. How to Prevent It Long-Term
- Add a lint rule that flags relative imports outside an installed package layout —
flake8-tidy-imports'TID252(relative imports banned) is one option if your team prefers explicit absolute imports everywhere; if you keep relative imports, the real guard is structural, not lint-based. - Put
python -m pkg.main(or the installed console script) in the README's "how to run this" section instead of a bare file path — most instances of this error are simply someone following stale or missing run instructions. - In CI, run the project the same way real users will run it:
pip install -e .then the console script orpython -m pkg, neverpython pkg/some_file.py, so a regression in packaging surfaces in CI instead of in a new contributor's terminal. - If your IDE (PyCharm, VS Code) "just works" on this file but the CLI doesn't, that's not a Python inconsistency — the IDE is silently running your file as
-munder the hood or mutatingsys.pathin its run configuration. Check the IDE's run configuration and mirror the same invocation in your terminal and CI, rather than trusting the IDE run button as the ground truth. - This error sits right next to two others worth knowing the boundary of:
ModuleNotFoundError: No module named 'x'(the package isn't importable at all — wrong interpreter or missing install) andImportError: cannot import name 'X' from partially initialized module(a circular import between two modules that are both correctly onsys.path). All three get pasted into search bars with nearly the same words, but the fixes don't overlap — check which one you actually have before applying a fix from the other two.
9. Key Takeaways
ImportError: attempted relative import with no known parent packagemeans the running module's__package__isNone— it fires during package-resolution, before Python even looks for the imported name on disk.python pkg/main.pyexecutes the file as__main__with no package context;python -m pkg.mainresolvespkg.mainthroughimportliband sets__package__correctly. Same file, same import, different outcome.- The fast fix is
python -m pkg.mainfrom the project root; the durable fix ispip install -e .so the package is always importable regardless of how it's launched. -P/PYTHONSAFEPATH(3.11+) can turn this into a different error (ModuleNotFoundError) if you combine it with-mfrom outside the project root — it's a security flag, not a relative-import fix.- Don't reach for
sys.pathhacks ortry/exceptimport fallbacks — they hide the real cause and tend to break again under the next runner (IDE, test framework, packaging tool) that launches the file differently.
CODELZ Newsletter
Join the newsletter to receive the latest updates in your inbox.