Python · lesson 11 of 34
Beginner
16- 1 Introduction to Python
- 2 Setting Up Python
- 3 Python Variables and Type System
- 4 Python Data Types
- 5 Python Operators
- 6 Python Strings
- 7 Control Flow in Python
- 8 Python Functions and Parameters
- 9 Python Data Structures
- 10 Python Collections
- 11 Python Modules and Packages
- 12 Object-Oriented Programming in Python
- 13 Encapsulation and Name Mangling in Python
- 14 Polymorphism and Duck Typing in Python
- 15 Abstraction with Abstract Base Classes in Python
- 21 Building a Python CLI Tool
Intermediate
12- 16 Multiple Inheritance and MRO in Python
- 17 Composition vs Inheritance in Python
- 18 Python Dataclasses
- 19 Error Handling in Python
- 20 File I/O, pathlib, and JSON in Python
- 22 Python Decorators and Closures
- 23 Python Generators
- 24 Python Context Managers
- 25 Python Concurrency
- 27 Testing Python Code
- 28 Python Security
- 34 Python Interview Prep
Advanced
6Python 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.