Site

Demonstrations — Running Demo Scripts from a Package

Tutorial D1.0  •  Python / Learn / Demonstrations

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:

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.
CommandWhat it does
python -m demos.basicRun basic.py with Demonstrations/ on sys.path
python -m demos.wordsRun words.py
python -m demos.shapesRun shapes.py
python demos/basic.pyRun 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

TermMeaning
packageA directory with __init__.py; importable as a module by its directory name
__init__.pyMarks a directory as a Python package; code in it runs on first import
python -mRun a module by dotted name; adds the current directory to sys.path
sys.pathList 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 specSuffix 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