Quick summary
Summarize this blog with AI
An absolute import names a module from the import search path. An explicit relative import starts with dots and is resolved using the current package. A filesystem-relative filename and a package-relative import are different things. This lesson supplies one complete package and tests it from two working directories using Python 3.12.14.
Create the same tiny package for every experiment
Create a new project folder containing the files below. The package initializer is an empty UTF-8 file named demo_pkg/__init__.py. No third-party dependencies are required.
project/
demo_pkg/
__init__.py
numbers.py
report.pydemo_pkg/numbers.py:
def total(values):
return sum(values)
demo_pkg/report.py demonstrates both import forms. It does not print when imported normally:
from demo_pkg.numbers import total as absolute_total
from .numbers import total as relative_total
def main():
print(absolute_total([2, 3]), relative_total([2, 3]))
if __name__ == "__main__":
main()
Run a module with its package context
From inside project, run:
python -m demo_pkg.report
Expected output:
5 5
The project directory is available for import, so Python finds demo_pkg and executes report with package context. Absolute imports use the full package path; from .numbers uses the sibling module in that package. A leading dot does not mean “whatever happens to be in my current working directory.”
Why direct script execution differs
From the same directory, python demo_pkg/report.py normally fails at the absolute import because the script directory, rather than its parent project directory, leads the search path. If you separately make demo_pkg importable, the explicit relative import still needs package context and can fail with “attempted relative import with no known parent package.” Use -m for this package entry point; do not paste sys.path mutations into every module.
The next reproducible probe creates the files in a temporary directory, strips inherited PYTHONPATH and PYTHONHOME settings for controlled tests, and compares two working directories. The supplied module code is repeated here so this probe is independently runnable.
from pathlib import Path
from tempfile import TemporaryDirectory
import os
import subprocess
import sys
with TemporaryDirectory() as temporary:
root = Path(temporary)
project = root / "project"
package = project / "demo_pkg"
package.mkdir(parents=True)
(package / "__init__.py").write_text("", encoding="utf-8")
(package / "numbers.py").write_text(
"def total(values):\n return sum(values)\n", encoding="utf-8")
(package / "report.py").write_text(
"from demo_pkg.numbers import total as absolute_total\n"
"from .numbers import total as relative_total\n"
"def main():\n print(absolute_total([2, 3]), relative_total([2, 3]))\n"
"if __name__ == '__main__':\n main()\n", encoding="utf-8")
environment = dict(os.environ)
for key in ("PYTHONPATH", "PYTHONHOME"):
environment.pop(key, None)
def run(arguments, cwd, extra_path=None):
selected = dict(environment)
if extra_path is not None:
selected["PYTHONPATH"] = str(extra_path)
return subprocess.run([sys.executable, *arguments], cwd=cwd,
env=selected, capture_output=True, text=True, timeout=5)
normal = run(["-m", "demo_pkg.report"], project)
direct = run(["demo_pkg/report.py"], project)
outside = run(["-m", "demo_pkg.report"], root)
explicit = run(["-m", "demo_pkg.report"], root, project)
direct_with_path = run(["demo_pkg/report.py"], project, project)
print("module:", normal.returncode, normal.stdout.strip())
print("direct fails:", direct.returncode != 0)
print("outside fails:", outside.returncode != 0)
print("explicit search path:", explicit.returncode, explicit.stdout.strip())
print("direct still lacks package:", "no known parent package" in direct_with_path.stderr)
Expected output:
module: 0 5 5
direct fails: True
outside fails: True
explicit search path: 0 5 5
direct still lacks package: True
Use installation for a reusable project
The temporary PYTHONPATH override demonstrates the search-path rule for one child process; it is not advice to permanently modify the machine’s environment. A real reusable project should declare packaging metadata and be installed into the selected virtual environment, making its package available from other working directories. That still does not make direct file execution equivalent to -m.
Regular packages use an initializer as shown; namespace packages are a supported exception, so an initializer is not a universal requirement for all packages. Avoid filenames such as json.py or pandas.py that shadow the module you meant to import. Inspect module.__file__ and sys.executable when the wrong copy loads. Circular imports require restructuring dependencies, not trying absolute and relative syntax at random.
References and next steps
Python modules and packages, Import system package context, Module search path.