🏗️ Phase 3 · OOP 🟡 Intermediate MODULE 15

Modules & Packages

⏱️ 40 min
📖 Theory + Code
🧩 5 Questions
🏗️ 1 Challenge
Phase 3 progress83%
🎯 What you'll learn: Creating and importing your own modules. All import styles. The __name__ == "__main__" guard. Building packages with __init__.py. Virtual environments with venv. Installing third-party packages with pip. A tour of the most important standard library modules.

Creating & Importing Modules

A module is simply a Python file. Any .py file can be imported by other files. When you write import math, Python finds math.py (or a compiled equivalent) and executes it, making its names available under the math namespace.

mathutils.py ← our module
PYTHON
# mathutils.py — a reusable math utilities module

PI = 3.14159265358979

def circle_area(r):
    """Return area of a circle."""
    return PI * r ** 2

def circle_circumference(r):
    return 2 * PI * r

def is_prime(n):
    """Return True if n is prime."""
    if n < 2: return False
    return all(n % i != 0 for i in range(2, int(n**.5)+1))

# __name__ guard: runs only when executed directly, not when imported
if __name__ == "__main__":
    print("Testing mathutils...")
    print(circle_area(5))    # 78.539...
    print(is_prime(17))       # True
main.py ← importing our module
PYTHON
# Style 1: import the whole module (namespace preserved)
import mathutils
print(mathutils.circle_area(3))   # 28.274...
print(mathutils.PI)                # 3.14159...

# Style 2: import specific names
from mathutils import is_prime, PI
print(is_prime(13))   # True — no mathutils. prefix needed

# Style 3: alias (useful for long names)
import mathutils as mu
print(mu.circle_area(7))

# Style 4: from ... import * (generally avoid — pollutes namespace)
# from mathutils import *

# ── Standard library imports ──────────────────
import math
print(math.sqrt(144))     # 12.0
print(math.factorial(10))  # 3628800

import random
print(random.choice(["Ali", "Sara", "Zara"]))
print(random.randint(1, 100))

from datetime import datetime, timedelta
now = datetime.now()
print(now.strftime("%d %B %Y"))
tomorrow = now + timedelta(days=1)
🎭
Why if __name__ == "__main__"?
When Python imports a module, it sets __name__ to the module's name (e.g. "mathutils"). When you run a file directly, __name__ is set to "__main__". This guard lets you write test/demo code in a module that only runs when you execute the file directly — not when it's imported. Every module you write should have this guard around runnable code.

Packages — Organising Multiple Modules

A package is a folder containing an __init__.py file and multiple modules. This lets you organise large projects into namespaced sub-modules. The __init__.py runs when the package is imported and controls what the package exports.

bwb_toolkit/← our package (folder)
    __init__.py← makes it a package; runs on import
    validators.py← input validation functions
    formatters.py← string/number formatting
    analytics.py← statistical functions
    data/← sub-package
        __init__.py
        csv_handler.py
        json_handler.py
main.py← imports from bwb_toolkit
bwb_toolkit/__init__.py
PYTHON
# __init__.py — package initialisation
# Import key names so users can write: from bwb_toolkit import validate_email
from .validators import validate_email, validate_age
from .formatters import format_currency, format_date

__version__ = "1.0.0"
__author__  = "BitWithBite"

# __all__ controls what 'from package import *' exports
__all__ = ["validate_email", "validate_age", "format_currency"]
bwb_toolkit/validators.py
PYTHON
# bwb_toolkit/validators.py
import re

def validate_email(email):
    """Return True if email is valid format."""
    pattern = re.compile(r'^[\w.-]+@[\w.-]+\.\w{2,}$')
    return bool(pattern.match(email))

def validate_age(age):
    """Return True if age is 5–120."""
    return isinstance(age, int) and 5 <= age <= 120


# main.py — importing from the package
from bwb_toolkit import validate_email   # from __init__
from bwb_toolkit.validators import validate_age  # direct submodule
from bwb_toolkit.data.csv_handler import load_csv  # nested subpackage

print(validate_email("ali@bwb.com"))   # True
print(validate_age(200))               # False

pip & Virtual Environments

pip is Python's package installer. It downloads and installs packages from PyPI (Python Package Index). A virtual environment is an isolated Python installation for each project — it keeps dependencies separate so Project A's packages don't conflict with Project B's.

terminal — venv & pip workflow
BASH
# ── Create a virtual environment ─────────────
python -m venv venv              # creates venv/ folder

# ── Activate it ──────────────────────────────
# Windows:
venv\Scripts\activate

# macOS/Linux:
source venv/bin/activate

# ── Install packages ─────────────────────────
pip install requests             # HTTP library
pip install pandas               # data analysis
pip install flask                # web framework
pip install python-dotenv        # .env file support

# ── Save dependencies ─────────────────────────
pip freeze > requirements.txt    # snapshot all installed packages

# ── Install from requirements.txt ────────────
pip install -r requirements.txt  # used by teammates or on server

# ── Useful pip commands ───────────────────────
pip list                         # show installed packages
pip show requests                # details about a package
pip install --upgrade pip        # upgrade pip itself
pip uninstall flask              # remove a package

# ── Deactivate ────────────────────────────────
deactivate
⚠️
Always add venv/ to your .gitignore
Never commit the venv folder to git — it contains thousands of files and is platform-specific. Instead commit requirements.txt. Anyone cloning your project runs pip install -r requirements.txt to recreate the environment. Also add __pycache__/ and *.pyc to .gitignore.

Standard Library — Must-Know Modules

Python ships with an enormous standard library — "batteries included." These are the modules every Python developer reaches for regularly:

os
OS interface: env vars, process, file ops
os.getenv("PATH")
sys
Interpreter: argv, exit, path, version
sys.argv[1]
pathlib
Object-oriented file paths
Path("data") / "file.csv"
datetime
Dates, times, timedeltas, formatting
datetime.now()
math
sqrt, factorial, log, pi, floor, ceil
math.sqrt(144)
random
RNG: randint, choice, shuffle, sample
random.shuffle(lst)
re
Regular expressions: match, search, sub
re.findall(r"\d+", s)
collections
Counter, defaultdict, OrderedDict, deque
Counter("hello")
itertools
chain, product, permutations, combinations
list(product("AB","12"))
functools
reduce, partial, lru_cache, wraps
@lru_cache(maxsize=128)
time
sleep, perf_counter, time (Unix timestamp)
time.sleep(1)
hashlib
MD5, SHA-256 and other hash functions
hashlib.sha256(b"text")
stdlib_examples.py
PYTHON
from collections import Counter, defaultdict
from functools import lru_cache
from itertools import combinations
import re, os

# Counter — count occurrences in one line
words   = "python is easy and python is fun".split()
counts  = Counter(words)
print(counts.most_common(3))   # [('python', 2), ('is', 2), ...]

# defaultdict — no KeyError on missing keys
groups  = defaultdict(list)
students= [("A", "Ali"), ("B", "Sara"), ("A", "Zara")]
for grade, name in students:
    groups[grade].append(name)   # no .setdefault() needed!
print(dict(groups))              # {'A': ['Ali', 'Zara'], 'B': ['Sara']}

# lru_cache — memoize expensive functions
@lru_cache(maxsize=128)
def fib(n):
    if n <= 1: return n
    return fib(n-1) + fib(n-2)
print(fib(50))   # 12586269025 — instant with caching

# re — regex
text = "Call us at 0300-1234567 or 042-3456789"
nums = re.findall(r'\d[\d-]+', text)
print(nums)      # ['0300-1234567', '042-3456789']

# os — environment and paths
print(os.getcwd())
print(os.getenv("HOME", "not set"))

# combinations
teams = list(combinations(["Ali","Sara","Zara","Omar"], 2))
print(teams)  # all 6 pairs
🧩 Knowledge Check
5 questions — Modules & Packages
1. What does the if __name__ == "__main__" guard do?
2. What file makes a directory a Python package?
3. What is the purpose of a virtual environment?
4. Which collection type auto-initialises missing keys?
5. What command saves all installed packages to a file for teammates?
🏗️
Challenge — Build a bwb_utils Package
Create a real multi-module package with proper structure
Task: Build a bwb_utils/ package with 3 modules and a proper __init__.py:

bwb_utils/strings.py — functions: title_case(text), count_words(text), is_palindrome(text) (ignore case/spaces), slugify(text) (lowercase, spaces→hyphens, remove non-alnum)

bwb_utils/numbers.py — functions: clamp(n, lo, hi), percentage(part, total), primes_up_to(n) using Sieve of Eratosthenes

bwb_utils/date_utils.py — functions: days_until(date_str) (days until a "YYYY-MM-DD" date), format_relative(date_str) returns "X days ago" or "in X days"

__init__.py — import key functions, set __version__ = "1.0.0". Write a main.py that imports from the package and tests all functions.
💡 Show hints
  • slugify: re.sub(r'[^a-z0-9-]', '', text.lower().replace(' ', '-'))
  • Sieve: start with list of True, mark multiples as False
  • days_until: (datetime.strptime(date_str, "%Y-%m-%d").date() - date.today()).days
  • Relative: f"in {d} days" if positive, f"{-d} days ago" if negative
bwb_utils/ — Sample Solution
PYTHON
# ── bwb_utils/strings.py ──────────────────────
import re

def title_case(text): return text.title()
def count_words(text): return len(text.split())
def is_palindrome(text):
    s = re.sub(r'[^a-z0-9]', '', text.lower())
    return s == s[::-1]
def slugify(text):
    return re.sub(r'[^a-z0-9-]', '', text.lower().replace(' ', '-'))

# ── bwb_utils/numbers.py ─────────────────────
def clamp(n, lo, hi): return max(lo, min(n, hi))
def percentage(part, total): return (part/total*100) if total else 0
def primes_up_to(n):
    sieve = [True] * (n+1)
    sieve[0] = sieve[1] = False
    for i in range(2, int(n**.5)+1):
        if sieve[i]: sieve[i*i::i] = [False]*len(sieve[i*i::i])
    return [i for i, p in enumerate(sieve) if p]

# ── bwb_utils/date_utils.py ───────────────────
from datetime import datetime, date
def days_until(date_str):
    target = datetime.strptime(date_str, "%Y-%m-%d").date()
    return (target - date.today()).days
def format_relative(date_str):
    d = days_until(date_str)
    if   d == 0:  return "today"
    elif d > 0:   return f"in {d} days"
    else:          return f"{-d} days ago"

# ── bwb_utils/__init__.py ─────────────────────
from .strings    import title_case, slugify, is_palindrome
from .numbers    import clamp, primes_up_to
from .date_utils import format_relative
__version__ = "1.0.0"

# ── main.py ───────────────────────────────────
import bwb_utils as bwb
print(bwb.slugify("Hello BitWithBite 2025!"))  # hello-bitwithbite-2025
print(bwb.is_palindrome("A man a plan a canal Panama")) # True
print(bwb.primes_up_to(30))   # [2,3,5,7,11,13,17,19,23,29]
print(bwb.clamp(150, 0, 100))  # 100
print(bwb.format_relative("2026-01-01"))  # e.g. "in 225 days"
🎉
Lesson 15 Complete!
Modules & packages mastered — you now know how professional Python projects are structured. Time to build Phase 3 projects!
← Course Home
Phase 3 · OOPLesson 15 of 6