Skip to content

Python · Modules, Packages & Environments

Python Packages & __main__ Explained — __init__.py, Running Modules, and Package Layout

3 min read Updated 2026-06-19 Share:

Practice Packages & __main__ interview questions

Python packages & main, explained

A package is just a directory of modules Python treats as a namespace. The mechanics — __init__.py, the __main__ idiom, and python -m — confuse a lot of people because they touch how Python both imports and runs code. Here's how the pieces fit.

What makes a package

A package is a directory Python can import. Traditionally it contains an __init__.py file; that file runs when the package is first imported and defines what the package exposes.

myapp/
    __init__.py        # marks/initialises the package
    models.py
    utils/
        __init__.py
        text.py
from myapp.utils.text import slugify   # dotted path follows the directory tree

__init__.py can be empty, or it can re-export names so callers get a clean API (from myapp.models import User instead of deep paths).

init.py and what to put in it

The __init__.py runs on import, so it's the place to set up the package's public surface. A common pattern is to lift key names up and declare __all__.

# myapp/__init__.py
from .models import User
from .utils.text import slugify

__all__ = ["User", "slugify"]    # what "from myapp import *" exposes

Keep it light — heavy work in __init__.py runs on every import of the package. Since Python 3.3, directories without __init__.py can act as namespace packages, but explicit __init__.py remains the clear default for regular packages.

The name == "main" idiom

Every module has a __name__. When run directly it's "__main__"; when imported it's the module's name. The guard lets a file be both an importable module and a runnable script.

# tool.py
def main():
    print("running")

if __name__ == "__main__":
    main()       # runs only when executed directly, not on import

Without the guard, the script's code would execute as a side effect of importing it — which is also why multiprocessing requires this guard.

Running packages with -m

python -m package runs a module as a script while keeping proper package context, so relative imports work. This is the right way to run code that lives inside a package.

python -m myapp.tool        # runs myapp/tool.py with myapp as its package
python script.py            # runs as top-level — relative imports would fail

-m adds the current directory to sys.path and sets up the package, which is exactly what direct python path/to/file.py execution does not do.

main.py makes a package runnable

If a package contains a __main__.py, then python -m package runs that file. This lets a whole package be executed like a command.

myapp/
    __init__.py
    __main__.py        # python -m myapp runs this
# myapp/__main__.py
from .cli import run
run()

This is how tools like python -m http.server and python -m pip work.

Modern project layout

Real projects use a src layout with a pyproject.toml declaring the package. Keeping code under src/ prevents accidentally importing from the working directory instead of the installed package.

myproject/
    pyproject.toml
    src/
        myapp/
            __init__.py
            ...
    tests/

After pip install -e ., import myapp works from anywhere, and tests run against the installed package rather than loose files.

Recap

A package is a directory of modules, marked and initialised by __init__.py (run on import — keep it light; use it to re-export a clean API). The __name__ == "__main__" guard lets a file act as both an importable module and a runnable script. Run code inside packages with python -m package so relative imports work, and add a __main__.py to make a package itself executable. For real projects, adopt the src layout with pyproject.toml and an editable install.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel