D1.0 What This Teaches
Python's module system lets you place shared library code in a package and write
small demo scripts that import from it. Running each demo with
python -m keeps the project root on the import path automatically.
This tutorial covers:
- Structuring a library as a Python package with
__init__.py
- Writing demo scripts in a
demos/ subdirectory
- Using
python -m demos.basic to run a demo from the project root
- Why
python -m is preferred over running the script directly
D1.1 Why a demos/ Directory?
A library module defines functions and classes but has no main entry point. A
demos/ directory holds short scripts that import from the library and
show how to use it. Keeping demonstrations separate from the library code and from
the test suite makes each role clear.
The pattern mirrors what Cargo's examples/ directory does for Rust
and what a CMake demos/ subdirectory does for C++: each demo is
self-contained and run individually.
D1.2 Project Layout
Demonstrations/
├── demo_lib/
│ └── __init__.py ← library package: functions and classes
└── demos/
├── __init__.py ← makes demos/ a package (required for -m)
├── basic.py
├── words.py
└── shapes.py
Both demo_lib/ and demos/ have __init__.py
so Python treats each as a package. Running with python -m from
Demonstrations/ places that directory on sys.path,
making import demo_lib resolve correctly.
D1.3 The Library Package - demo_lib/__init__.py
# demo_lib/__init__.py - public API for the Demonstrations package.
import math
def add(a, b):
return a + b
def clamp(value, lo, hi):
if value < lo:
return lo
if value > hi:
return hi
return value
def word_count(s):
return len(s.split())
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
def circumference(self):
return 2 * math.pi * self.radius
def __repr__(self):
return f"Circle(r={self.radius})"
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
def is_square(self):
return self.width == self.height
def __repr__(self):
return f"Rectangle({self.width}x{self.height})"
Placing all public names in __init__.py means importers write
from demo_lib import add rather than navigating a deeper module path.
D1.4 Demo: demos/basic.py
# basic.py - demonstrates add and clamp from demo_lib.
# Run from Demonstrations/ with: python -m demos.basic
from demo_lib import add, clamp
def main():
print("=== basic demo ===")
print(f"add(7, 3) = {add(7, 3)}")
print(f"clamp(25, 0, 20) = {clamp(25, 0, 20)}")
print(f"clamp(-5, 0, 20) = {clamp(-5, 0, 20)}")
print(f"clamp(10, 0, 20) = {clamp(10, 0, 20)}")
if __name__ == "__main__":
main()
The if __name__ == "__main__" guard runs main() only when
the script is executed directly. If another module imports demos.basic,
the guard prevents the output from appearing at import time.
D1.5 Demo: demos/words.py
# words.py - demonstrates word_count from demo_lib.
# Run from Demonstrations/ with: python -m demos.words
from demo_lib import word_count
def main():
text = "the quick brown fox jumps over the lazy dog the fox"
print("=== words demo ===")
print(f"text: \"{text}\"")
print(f"word_count: {word_count(text)}")
if __name__ == "__main__":
main()
D1.6 Demo: demos/shapes.py
# shapes.py - demonstrates Circle and Rectangle from demo_lib.
# Run from Demonstrations/ with: python -m demos.shapes
from demo_lib import Circle, Rectangle
def main():
print("=== shapes demo ===")
c = Circle(5.0)
print("Circle r=5:")
print(f" area = {c.area():.4f}")
print(f" circumference = {c.circumference():.4f}")
r = Rectangle(4.0, 6.0)
print("Rectangle 4x6:")
print(f" area = {r.area():.1f}")
print(f" perimeter = {r.perimeter():.1f}")
print(f" is_square = {r.is_square()}")
sq = Rectangle(5.0, 5.0)
print("Rectangle 5x5:")
print(f" is_square = {sq.is_square()}")
if __name__ == "__main__":
main()
:.4f inside an f-string formats a float to four decimal places.
This is the same mini-language used by format() and
str.format().
D1.7 Running the Demos
Run all demos from the Demonstrations/ directory:
python -m demos.basic
python -m demos.words
python -m demos.shapes
The -m flag tells Python to run a module by dotted name rather than a
file path. Python adds the current working directory to sys.path before
executing the module, so import demo_lib resolves to
Demonstrations/demo_lib/.
Running with a file path instead (python demos/basic.py) adds
Demonstrations/demos/ to sys.path - the script's own
directory - so import demo_lib would not resolve unless you also
manipulate sys.path manually. The -m form avoids that
problem.
| Command | What it does |
python -m demos.basic | Run basic.py with Demonstrations/ on sys.path |
python -m demos.words | Run words.py |
python -m demos.shapes | Run shapes.py |
python demos/basic.py | Run basic.py directly - import demo_lib will fail without sys.path fix |
D1.8 Expected Outputs
python -m demos.basic
=== basic demo ===
add(7, 3) = 10
clamp(25, 0, 20) = 20
clamp(-5, 0, 20) = 0
clamp(10, 0, 20) = 10
python -m demos.words
=== words demo ===
text: "the quick brown fox jumps over the lazy dog the fox"
word_count: 11
python -m demos.shapes
=== shapes demo ===
Circle r=5:
area = 78.5398
circumference = 31.4159
Rectangle 4x6:
area = 24.0
perimeter = 20.0
is_square = False
Rectangle 5x5:
is_square = True
D1.9 Exercise
Exercise
- Add a
unique_words(s) function to demo_lib/__init__.py
that returns a sorted list of distinct words from the string.
- Create
demos/stats.py that calls both word_count
and unique_words on several strings and prints both results.
- Run the new demo with
python -m demos.stats and verify the
output.
D1.10 Common Mistakes
Running with python demos/basic.py instead of python -m demos.basic
Running via file path places demos/ on sys.path, not
Demonstrations/. The import demo_lib statement then fails
with ModuleNotFoundError. Always use python -m demos.basic
from Demonstrations/.
Missing __init__.py in demos/
Without demos/__init__.py, Python does not treat demos/
as a package and python -m demos.basic fails with
No module named demos. The file can be empty - its presence is enough.
Forgetting the if __name__ == "__main__" guard
Without the guard, any code at module level runs when the file is imported. If
another script imports demos.basic to reuse a helper function, all
the print statements execute immediately. The guard limits execution to direct runs.
Importing from the wrong level
from __init__ import add # error: not a valid import path
from demo_lib import add # correct
You never import from __init__ by name. The package directory name
(demo_lib) is the import path.
D1.11 Key Terms
| Term | Meaning |
| package | A directory with __init__.py; importable as a module by its directory name |
| __init__.py | Marks a directory as a Python package; code in it runs on first import |
| python -m | Run a module by dotted name; adds the current directory to sys.path |
| sys.path | List of directories Python searches when resolving import statements |
| if __name__ == "__main__" | Guard that runs a block only when the file is executed directly, not imported |
| f-string format spec | Suffix after : in an f-string placeholder, e.g. :.4f for 4 decimal places |
| __repr__ | Dunder method that returns a developer-readable string; used by repr() and in the REPL |