Entry Points and main

Updated

September 8, 2026

Entry Points and __main__

An entry point is the first function that runs when a human (or a service) starts your program. The boring default is a main() plus if __name__ == "__main__". For a package, add __main__.py so uv run python -m desk works. For a command people type, declare [project.scripts] in pyproject.toml.

Mental model

When you run a file, Python sets __name__ to "__main__". When another file imports it, __name__ is the module name. The main guard is the if that calls main only in the first case.

uv run python -m desk looks for a package desk and runs desk/__main__.py (or desk.py if there is no package). The -m form keeps the package context, so relative imports work.

[project.scripts] in pyproject.toml maps a command name to module:function. After the project is installed into the environment, uv run desk calls that function with no arguments. main should return an int exit code or None.

Worked examples

Case 1: The main guard

Save as open_desk.py. Importing this file later will not print.

# open_desk.py
def main():
    print("desk is open")
    return 0


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

Run:

uv run python open_desk.py

Output:

desk is open

SystemExit turns the return code into the process status. 0 means success.

Case 2: python -m on a package with __main__.py

Create a folder desk/ next to nothing else you care about, and save these three files inside it. Run from the parent of desk/.

Save as desk/__init__.py (empty is fine; a comment is enough):

# desk/__init__.py

Save as desk/cli.py:

# desk/cli.py
def main():
    print("desk is open")
    return 0

Save as desk/__main__.py. This file is the -m entry.

# desk/__main__.py
from .cli import main

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

Run:

uv run python -m desk

Output:

desk is open

from .cli import main works because -m desk loaded a package. cli.py stays importable by tests.

Case 3: [project.scripts] in pyproject.toml

Put the same desk/ package inside a project folder desk_app/ with a pyproject.toml. Layout:

  • desk_app/pyproject.toml
  • desk_app/desk/__init__.py
  • desk_app/desk/cli.py
  • desk_app/desk/__main__.py (optional here; the script entry does not need it)

Save as desk_app/pyproject.toml:

# pyproject.toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "desk"
version = "0.1.0"
requires-python = ">=3.14"
description = "A tiny desk command"

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

Reuse desk/cli.py from Case 2 (def main(): that prints and returns 0). From desk_app/:

uv run desk

Output:

desk is open

uv run builds the project into the environment and puts a desk command on the path. The value "desk.cli:main" means “import desk.cli, call main.”

You can keep __main__.py as well. Then both uv run desk and uv run python -m desk start the same main.

The trap

Running a file inside the package by path drops the package. Relative imports die.

Save as desk/tickets.py:

# desk/tickets.py
class Ticket:
    def __init__(self, id, table):
        self.id = id
        self.table = table

Save as desk/cli.py:

# desk/cli.py
from .tickets import Ticket


def main():
    t = Ticket(7, 12)
    print(f"ticket {t.id} → table {t.table}")
    return 0

Save as desk/__main__.py (same as Case 2) and an empty desk/__init__.py. Run the CLI file by path:

uv run python desk/cli.py

Output (the process exits non-zero):

Traceback (most recent call last):
  File "desk/cli.py", line 2, in <module>
    from .tickets import Ticket
ImportError: attempted relative import with no known parent package

The fix is not to delete the dot. Keep the relative import and start the package:

uv run python -m desk

Output:

ticket 7 → table 12

The boring rule

  • Every program file: def main(): and if __name__ == "__main__": raise SystemExit(main()).
  • Packages people run: __main__.py that calls cli.main.
  • Commands people type: [project.scripts] name = "pkg.cli:main".
  • Start the package with python -m desk, never python desk/cli.py.
  • main takes no arguments when it is a console script. Parse sys.argv inside it, or use argparse.

Try this

  1. In open_desk.py, print __name__ inside main. Run the file. Then from a second file import open_desk (no call) and confirm it prints nothing.
  2. Add desk/tickets.py and print a ticket label from desk/cli.py using a relative import. Start it with uv run python -m desk.
  3. Change [project.scripts] so the command is open-desk = "desk.cli:main". Run uv run open-desk.
  4. Add a main guard to desk/cli.py so uv run python -m desk.cli also prints. Keep __main__.py delegating to the same main.