Entry Points and main
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.pyOutput:
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__.pySave as desk/cli.py:
# desk/cli.py
def main():
print("desk is open")
return 0Save 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 deskOutput:
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.tomldesk_app/desk/__init__.pydesk_app/desk/cli.pydesk_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 deskOutput:
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 = tableSave 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 0Save 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.pyOutput (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 deskOutput:
ticket 7 → table 12
The boring rule
- Every program file:
def main():andif __name__ == "__main__": raise SystemExit(main()). - Packages people run:
__main__.pythat callscli.main. - Commands people type:
[project.scripts]name = "pkg.cli:main". - Start the package with
python -m desk, neverpython desk/cli.py. maintakes no arguments when it is a console script. Parsesys.argvinside it, or useargparse.
Try this
- In
open_desk.py, print__name__insidemain. Run the file. Then from a second fileimport open_desk(no call) and confirm it prints nothing. - Add
desk/tickets.pyand print a ticket label fromdesk/cli.pyusing a relative import. Start it withuv run python -m desk. - Change
[project.scripts]so the command isopen-desk = "desk.cli:main". Runuv run open-desk. - Add a main guard to
desk/cli.pysouv run python -m desk.clialso prints. Keep__main__.pydelegating to the samemain.