Bobobk

Practical Python, Linux and automation guides

Fix Python Relative Import Errors with -m, Package Entry Points, and a Linux Launcher

2026-11-02 Technology
Fix Python Relative Import Errors with -m, Package Entry Points, and a Linux Launcher
On this page

After splitting a Python script into a package, python3 triage_demo/cli.py fails with ImportError: attempted relative import with no known parent package. The files exist, but the launch method has not established the package context; installing another dependency is not necessarily the answer.

This beginner-to-intermediate tutorial reproduces the error with a text-line counter, switches to a python3 -m entry point, and adds a Linux launcher with a stable working directory. Tests cover a different starting directory, filenames with spaces, and error exits. No third-party packages or system configuration changes are needed.

1. Method: distinguish file and module entry points

The Python tutorial on intra-package references explains how dotted imports relate to the current package. Here, from .helpers import count_lines refers to helpers in the same package, not a file in the current working directory.

  1. python3 triage_demo/cli.py runs a file directly. In this example, no parent package is available for the relative import.
  2. python3 -m triage_demo.cli starts by module name, preserving the triage_demo package context.
  3. python3 -m triage_demo runs the package’s __main__.py, providing a shared command entry point.

The official -m documentation specifies a module name without .py or path separators. This tutorial runs an uninstalled source tree without a global PYTHONPATH or business-code edits to sys.path.

2. Prepare the environment and a minimal package

Use an existing Python 3.9 or newer interpreter and Linux sh, bash, or zsh. The examples were verified with Python 3.11.4. Keep all files in a new demo directory; choose another directory name when repeating the exercise.

  1. Run these commands in the same terminal:
python3 --version
python3 --help
mkdir relative-import-demo
cd relative-import-demo
demo_root="$PWD"
mkdir triage_demo data
: > triage_demo/__init__.py
printf 'alpha\nbeta\ngamma\n' > data/items.txt
  1. Save this as triage_demo/helpers.py. It counts text lines, including blank lines, rather than CSV records:
from pathlib import Path


def count_lines(filename):
    with Path(filename).open(encoding="utf-8") as source:
        return sum(1 for _ in source)
  1. Save this as triage_demo/cli.py:
import argparse
import sys

from .helpers import count_lines


def main():
    parser = argparse.ArgumentParser(description="Count UTF-8 text lines.")
    parser.add_argument("input", help="text file to read")
    args = parser.parse_args()
    try:
        lines = count_lines(args.input)
    except (OSError, UnicodeError) as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 1
    print(f"lines={lines}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

The empty __init__.py makes this example a regular package. main() handles arguments; read errors go to stderr with exit code 1, while argparse returns 2 for a missing argument. For multiple operations, see argparse subcommands and parameter validation.

3. Reproduce the error, then fix the launch with -m

  1. Stay in relative-import-demo and deliberately run the file directly:
if python3 triage_demo/cli.py data/items.txt; then
  printf 'Unexpected success: inspect the package and environment\n'
else
  printf 'Expected failure: direct file execution lost package context\n'
fi

The traceback should contain:

ImportError: attempted relative import with no known parent package

The if handles this expected failure so a shell using set -e does not stop early. Adding if __name__ == "__main__" alone does not restore package context: the example already has that guard and still fails during import.

  1. Keep the import unchanged and launch by module name instead:
python3 -m triage_demo.cli --help
python3 -m triage_demo.cli data/items.txt

The first command prints help; the second prints lines=3 and exits 0. Inspect the module name and parent package during a normal import with:

python3 -c 'import triage_demo.cli as m; print(m.__name__); print(m.__package__)'

Expect triage_demo.cli and triage_demo, respectively. Avoid changing the import to bare from helpers import ... just to accommodate one launch method; that changes module identity and lookup location.

4. Add a package entry point and a stable Linux launcher

  1. Save this as triage_demo/__main__.py. It delegates to the existing main() rather than copying business logic:
from .cli import main

if __name__ == "__main__":
    raise SystemExit(main())

The Python package entry-point documentation describes __main__.py. This example includes an entry guard so importing this file also avoids parsing arguments or starting a job.

python3 -m triage_demo data/items.txt

Expect lines=3 again. -m does not discover projects anywhere on disk. This package is uninstalled, so run from its parent directory; the same command elsewhere can fail to find triage_demo.

  1. Save launch.sh in the demo’s root directory:
#!/bin/sh
set -eu
project_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
cd -- "$project_dir"
exec "${PYTHON:-python3}" -m triage_demo "$@"

The launcher enters its own directory and then executes the module. "$@" preserves argument boundaries, and exec passes Python’s exit status to the caller. Set PYTHON to the absolute path of an existing interpreter; otherwise the launcher uses python3 from PATH. This variable is a single executable path, not a command string containing extra flags.

  1. Invoke it from another working directory:
chmod u+x launch.sh
(
  cd /
  sh "$demo_root/launch.sh" data/items.txt
)

Expect lines=3. Parentheses keep cd / inside a subshell. The launcher interprets relative input paths from the project root; pass an absolute path for an input in the caller’s directory. Run only trusted projects, do not invoke this example through a symlink, and do not treat it as an installer for every package layout.

Real scheduled jobs also need a stable interpreter and environment; see troubleshooting Linux job paths and environment. This tutorial only demonstrates launching and does not change a scheduler.

5. Automatically verify success and failure branches

Save this as verify_demo.py beside launch.sh. It uses the package created above, puts test inputs in its own temporary directory, and cleans up afterward without deleting your demo directory.

from pathlib import Path
import os
import subprocess
import sys
import tempfile

root = Path(__file__).resolve().parent
env = os.environ.copy()
for key in list(env):
    if key.startswith("PYTHON"):
        del env[key]
env["PYTHON"] = sys.executable


def run(args, cwd):
    return subprocess.run(
        args, cwd=cwd, env=env, capture_output=True,
        text=True, timeout=10, check=False,
    )


with tempfile.TemporaryDirectory(prefix="module-entry-test-") as tmp:
    outside = Path(tmp)
    source = outside / "input with spaces.txt"
    source.write_text("alpha\n\ngamma\n", encoding="utf-8")
    path = str(source)

    direct = run([sys.executable, "triage_demo/cli.py", path], root)
    assert direct.returncode == 1 and not direct.stdout, direct
    assert "attempted relative import" in direct.stderr, direct

    for module in ["triage_demo.cli", "triage_demo"]:
        good = run([sys.executable, "-m", module, path], root)
        assert (good.returncode, good.stdout, good.stderr) == (0, "lines=3\n", ""), good

    foreign = run([sys.executable, "-m", "triage_demo", path], outside)
    assert foreign.returncode != 0 and "No module named triage_demo" in foreign.stderr, foreign

    wrapped = run(["sh", str(root / "launch.sh"), path], outside)
    assert (wrapped.returncode, wrapped.stdout, wrapped.stderr) == (0, "lines=3\n", ""), wrapped

    help_result = run([sys.executable, "-m", "triage_demo", "--help"], root)
    assert help_result.returncode == 0 and "Count UTF-8 text lines." in help_result.stdout, help_result
    no_arg = run([sys.executable, "-m", "triage_demo"], root)
    assert no_arg.returncode == 2 and "required" in no_arg.stderr, no_arg
    missing = run([sys.executable, "-m", "triage_demo", str(outside / "missing.txt")], root)
    assert missing.returncode == 1 and missing.stderr.startswith("ERROR:"), missing
    bad = outside / "bad.txt"
    bad.write_bytes(b"\xff")
    encoding = run([sys.executable, "-m", "triage_demo", str(bad)], root)
    assert encoding.returncode == 1 and "ERROR:" in encoding.stderr, encoding
    imported = run([sys.executable, "-c", "import triage_demo.cli; import triage_demo.__main__; print('import OK')"], root)
    assert (imported.returncode, imported.stdout, imported.stderr) == (0, "import OK\n", ""), imported

print("OK: direct failure, module entry, package entry, foreign cwd, launcher, help, errors, safe import")
python3 verify_demo.py

Expected output:

OK: direct failure, module entry, package entry, foreign cwd, launcher, help, errors, safe import

The tests check direct-file failure, both module entry points, failure from an unrelated directory, launcher success, a path with spaces, blank-line counting, help, missing arguments, missing files, invalid UTF-8, and safe imports. Child processes have a ten-second timeout. Inherited PYTHON* settings are removed, and the launcher is given the current interpreter to isolate external path settings.

6. Common errors and direct fixes

Symptom Action
attempted relative import with no known parent package Run python3 -m triage_demo.cli from the project root instead of executing a package file directly.
No module named triage_demo Confirm that the current directory contains triage_demo/, or call launch.sh by absolute path. For an installed project, check its interpreter environment.
No module named triage_demo.__main__ Create the __main__.py from section 4 or run -m triage_demo.cli directly.
attempted relative import beyond top-level package Check the number of dots and the actual package hierarchy; a different launch method cannot fix an incorrect package relationship.
The package loads, but the input file is missing The launcher resolves relative inputs from the project root. Pass an absolute input path.
The module name is correct, but its source is wrong Inspect local names and follow module shadowing and import-path troubleshooting.

Do not install a same-named package globally or delete every __pycache__ directory to address one import error. Check the command, working directory, and full traceback before distinguishing an entry-point problem from shadowing or a dependency environment issue.

7. Summary

Relative imports need the right package context; a file’s existence does not make direct execution the correct entry point. Keep business code in modules, use -m with a thin __main__.py, and connect automation through a launcher with a stable working directory. Verify the original failure as well as successful output to catch jobs that work in a terminal but cannot find their package elsewhere.

Tags

1024 2fa ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation background backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn checkpoint cli cloudflare codex cofoldarena concurrency configparser configuration copy cprofile cpu monitoring cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging decimal deployment desktop-app devtools difflib disk-space disown dns docker dovecot download economics electron encoding esm esm2 esm3 esmc esmfold2 etag evaluation fable faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io file-management filecmp filenames files flashppi flask folium frontend game generator git github-actions google google-research gpt-6.1-sol grep gzip harness heapq hls html http hugo imports indexnow ipaddress jailbreak javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp memory mirror mmcif model-evaluation monitoring mp3 mp4 mpnn multiomics multiprocessing mutation mysql nanobert nanobodies networking networkx nginx normalize numpy ollama omegatherm pandas password pathlib pdb pep-723 performance permissions phaser pillow pip postfix preprocessing print probability prompt-engineering protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security seo sequence-identity sha256 shell signals sitemap sklearn socket solana somaticsignatures sorting spl sqlite ssh standardize static-site subprocess symlink sysadmin system-one tarfile tcp tempfile tensorflow tkinter tls toml totp tracemalloc trade tron tronpy troubleshooting turtle typesafe-ai ubuntu unicode upsert urllib usdt utf-8 uv venv vhhbert vite wallet web-tools webp wordcloud wordpress workflow xml yaml zipfile zoneinfo 迅雷解析

友情链接