Skip to main content
Python · lesson 11 of 34
Python beginner Lesson 11 of 34

Python Modules and Packages

Build a real package step by step: import styles, the __name__ guard, __init__.py and __all__, relative imports, sys.path, venv and pyproject.toml.

By SkillByExample · Updated

A module is any .py file; a package is a directory of modules. This lesson builds a small package on disk and imports it every way you’ll meet in real code. Run the examples from one empty directory; output is from Python 3.13.

Importing a module

import runs a module once, caches it, and binds a name to it.

# greetings.py
print(f"loading greetings (__name__ = {__name__!r})")

def hello(name: str) -> str:
    return f"Hello, {name}!"
import sys
import greetings
import greetings as g
from greetings import hello

print(hello("Ana"), g is greetings, "greetings" in sys.modules)

import math
from math import sqrt as root
print(math.floor(3.7), root(16))
$ python main.py
loading greetings (__name__ = 'greetings')
Hello, Ana! True True
3 4.0

The module’s top-level print ran only once even though greetings was imported three ways: after the first import it lives in sys.modules, and later imports reuse it. Inside an imported module, __name__ is the module’s name.

The __name__ guard

When you run a file directly, its __name__ is "__main__" instead.

# utils.py
def add(a: int, b: int) -> int:
    return a + b

if __name__ == "__main__":
    print("running as a script:", add(2, 3))
import utils
print("imported:", utils.add(10, 20))
$ python utils.py
running as a script: 5
$ python use_utils.py
imported: 30

The same file is a script when run and a quiet library when imported. Without the guard, every import utils would also run the demo code.

A package with __init__.py and __all__

__init__.py runs when the package is imported and decides what it exposes.

# myapp/__init__.py
"""A tiny example package."""
# myapp/auth/tokens.py
def generate_token(user: str) -> str:
    return f"tok-{user}"

def _secret_salt() -> str:          # leading underscore: private by convention
    return "salt"
# myapp/auth/__init__.py
from .tokens import generate_token

__all__ = ["generate_token"]
from myapp.auth import generate_token
from myapp.auth.tokens import generate_token as direct
import myapp.auth as auth

print(generate_token("ana"), generate_token is direct)
print(auth.__all__)

ns: dict = {}
exec("from myapp.auth.tokens import *", ns)
print(sorted(k for k in ns if not k.startswith("__")))
$ python use_package.py
tok-ana True
['generate_token']
['generate_token']

Re-exporting in auth/__init__.py gives callers the short path myapp.auth.generate_token, and both paths give the same function object. import * skipped _secret_salt because of its leading underscore; when a module defines __all__, that list decides instead.

Relative imports

Inside a package, a leading dot means “this package” and two dots mean “the parent”.

# myapp/utils.py
def format_response(body: str) -> str:
    return f"[200] {body}"
# myapp/api.py
from .auth import generate_token
from .utils import format_response

def login(user: str) -> str:
    return format_response(generate_token(user))

if __name__ == "__main__":
    print(login("bo"))
$ python -m myapp.api
[200] tok-bo
$ python myapp/api.py
Traceback (most recent call last):
  File "/home/user/py-demo/modules/myapp/api.py", line 2, in <module>
    from .auth import generate_token
ImportError: attempted relative import with no known parent package

python -m myapp.api runs the file as part of the package, so .auth resolves. Running the file by path makes it a standalone script with no parent package, and the relative import fails — the most common packaging error beginners hit.

Where Python looks: sys.path

Imports search the directories in sys.path, in order; the first match wins.

# shadow/json.py
print("this is the local json.py, not the standard library!")
import sys
print(sys.path[0])

import json
print(hasattr(json, "dumps"))
$ python shadow/shadow.py
/home/user/py-demo/modules/shadow
this is the local json.py, not the standard library!
False

The script’s own directory is sys.path[0], ahead of the standard library, so a file named json.py beside your script shadows the real json module. If an import suddenly lacks attributes it should have, look for a file with the same name in your project.

Optional dependencies

Catch ImportError (or its subclass ModuleNotFoundError) to fall back when a package isn’t installed.

try:
    import ujson_not_installed as fastjson
except ModuleNotFoundError as e:
    print("fallback:", e)
    import json as fastjson

print(fastjson.dumps({"ok": True}))
$ python optional.py
fallback: No module named 'ujson_not_installed'
{"ok": true}

Virtual environments and pip

A virtual environment is a private site-packages directory for one project.

$ python -m venv .venv
$ .venv/bin/python -c "import sys; print(sys.prefix == sys.base_prefix)"
False
$ .venv/bin/python -m pip list --format=freeze
pip==26.2.1

sys.prefix differs from sys.base_prefix inside a venv, which is how tools detect one. A fresh venv contains only pip; whatever you install with .venv/bin/python -m pip install ... (or after source .venv/bin/activate) stays out of every other project. On Windows the folder is .venv\Scripts\ instead of .venv/bin/.

Packaging with pyproject.toml

pyproject.toml declares the project’s metadata, dependencies and console commands.

# pyproject.toml
[build-system]
requires = ["setuptools>=69"]
build-backend = "setuptools.build_meta"

[project]
name = "myapp"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = []

[project.optional-dependencies]
dev = ["pytest>=8"]

[project.scripts]
myapp-login = "myapp.api:main"

[tool.setuptools]
packages = ["myapp", "myapp.auth"]
# myapp/api.py
from .auth import generate_token
from .utils import format_response

def login(user: str) -> str:
    return format_response(generate_token(user))

def main() -> None:
    print(login("installed"))
$ .venv/bin/python -m pip install -q -e .
$ .venv/bin/myapp-login
[200] tok-installed
$ cd /tmp && /home/user/py-demo/modules/.venv/bin/python -c "import myapp.api; print(myapp.api.login('anywhere'))"
[200] tok-anywhere

pip install -e . (“editable”) installs the package into the venv while pointing at your source folder, so edits take effect without reinstalling. The [project.scripts] entry became a real myapp-login command, and the package now imports from any directory — no sys.path hacks. pip install -e ".[dev]" would add the optional dev dependencies too.

Practice

1. Add a __version__ = "0.1.0" attribute to myapp/__init__.py and print it from a script.

Solution
# myapp/__init__.py
"""A tiny example package."""
__version__ = "0.1.0"
import myapp
print(myapp.__version__)
$ python version.py
0.1.0

2. Show that a module’s top-level code runs only once by importing greetings inside a loop three times.

Solution
# greetings.py
print("loading greetings")
import importlib

for _ in range(3):
    import greetings
print("now reload:")
importlib.reload(greetings)
$ python import_once.py
loading greetings
now reload:
loading greetings

Only importlib.reload forces the module’s code to run again.

3. Give myapp/auth/tokens.py a __all__ = ["generate_token"] and also a public helper def verify(token): .... Which names does from myapp.auth.tokens import * import now?

Solution
# myapp/auth/tokens.py
__all__ = ["generate_token"]

def generate_token(user: str) -> str:
    return f"tok-{user}"

def verify(token: str) -> bool:
    return token.startswith("tok-")
ns: dict = {}
exec("from myapp.auth.tokens import *", ns)
print(sorted(k for k in ns if not k.startswith("__")))

from myapp.auth.tokens import verify
print(verify("tok-ana"))
$ python star_all.py
['generate_token']
True

With __all__ present, import * brings in only the listed names — verify is excluded, though an explicit import still works.

4. Print which file the json module was loaded from, once normally and once with a local json.py present, using json.__file__.

Solution
import os, subprocess, sys, tempfile

code = "import json; print('stdlib' if 'python3' in json.__file__ else 'local', json.__file__.rsplit('/', 1)[-1])"
with tempfile.TemporaryDirectory() as d:
    print(subprocess.run([sys.executable, "-c", code], cwd=d, capture_output=True, text=True).stdout, end="")
    open(os.path.join(d, "json.py"), "w").close()
    print(subprocess.run([sys.executable, "-c", code], cwd=d, capture_output=True, text=True).stdout, end="")
$ python which_json.py
stdlib __init__.py
local json.py

The standard library’s json is a package (json/__init__.py); once an empty json.py exists in the working directory, it wins.

Frequently Asked Questions

What's the difference between a module and a package?
A module is a single .py file. A package is a directory of modules, normally with an __init__.py file, that you import with dotted names such as myapp.auth.tokens. Importing a package runs its __init__.py.
When should I use 'from x import y' vs 'import x'?
import x keeps the namespace visible at every call site (x.y), which reads clearly when you use several names from x. from x import y is fine for a few frequently used names. Avoid from x import * outside interactive sessions, because it hides where names come from.
What is __all__ used for?
__all__ is a list of the public names in a module. It controls exactly what from module import * brings in, and it documents the intended API for readers and tools; names not listed are still importable explicitly.
Why does a module's code only run once when imported twice?
The first import executes the module and stores it in sys.modules. Every later import of the same name, from anywhere in the program, returns that cached module object without running its code again.
Why do I get 'attempted relative import with no known parent package'?
Relative imports such as from .tokens import x only work inside a package. Running the file directly as python myapp/auth/routes.py treats it as a top-level script; run it as a module instead with python -m myapp.auth.routes from the project root.