
Python Course
Modules and Packages in Python
As your programs grow, you cannot put everything into one file. Professional Python projects split code across many files — each file focused on one area of responsibility. Python's module system makes this possible. A module is simply a .py file that contains functions, variables, and classes you can import and use anywhere.
Python also ships with an enormous Standard Library — hundreds of ready-to-use modules for maths, dates, file operations, randomness, data structures, and much more. On top of that, the Python community has built thousands of free third-party packages you can install in seconds. This ecosystem is one of the biggest reasons Python is so popular across data science, web development, automation, and AI.
What is a Module — and Why It Exists
Every .py file is a module. When you write import math, Python finds the file math.py (or the compiled equivalent) and makes everything inside it available to your program. You do not rewrite a square root function from scratch — you import one that has been written, tested, and optimised already.
- Reusability — write a helper once, import it in any project.
- Organisation — each file has one clear purpose, making large projects navigable.
- Collaboration — team members work on separate modules without stepping on each other's code.
- Standard Library — hundreds of tested, documented modules included with every Python installation.
Importing a Module
The import keyword loads a module. Once loaded, you access its contents using dot notation — module_name.function_name(). The dot makes it clear exactly where each function comes from, which prevents naming conflicts when you use many modules together.
import math
print(math.sqrt(49)) # 7.0 — square root
print(math.pi) # 3.141592...
print(math.floor(4.9)) # 4 — always rounds down
print(math.ceil(4.1)) # 5 — always rounds up
print(math.pow(2, 8)) # 256.0
print(math.factorial(6)) # 720 — 6!
print(math.log(100, 10)) # 2.0 — log base 10
print(math.fabs(-9.5)) # 9.5 — absolute value as float
print(math.e) # 2.718... — Euler's number
math.floor()always rounds down andmath.ceil()always rounds up — regardless of the decimal, unlike Python's built-inround()which rounds to nearest.math.piandmath.eare constants — access them without parentheses.math.log(100, 10)= 2 because 10² = 100. The second argument is the base.
Importing Specific Items with from
If you only need a few things from a large module, import just those items. They become available directly — no module prefix needed.
from math import sqrt, pi, factorial
# Use directly — no "math." prefix
print(sqrt(81)) # 9.0
print(pi) # 3.14159...
print(factorial(5)) # 120
# Import multiple items
from math import floor, ceil, log
print(floor(9.9)) # 9
print(ceil(9.1)) # 10
print(log(1000, 10)) # 3.0
- Best practice: import only what you need. It keeps your code readable and avoids accidentally overwriting names.
- Avoid
from math import *— it imports everything at once, which can silently overwrite variables you already have with the same name. This leads to bugs that are hard to trace.
Importing with an Alias
Use as to give a module or function a shorter nickname. This is especially useful for long module names — and some aliases are so universal they are treated as industry convention.
import math as m
print(m.sqrt(64)) # 8.0
print(m.pi) # 3.14159...
from math import factorial as fact
print(fact(7)) # 5040
# These are standard industry aliases — every Python developer uses them:
# import numpy as np ← data science / arrays
# import pandas as pd ← data analysis / dataframes
# import matplotlib.pyplot as plt ← charts and graphs
# import tensorflow as tf ← deep learning
print("Aliases make long names shorter and code cleaner.")
The random Module
The random module generates random numbers and makes random selections. It is used in games, simulations, testing, sampling, and generating tokens or one-time codes.
import random
# Random integer — both endpoints included
print(random.randint(1, 10))
# Random float between 0.0 and 1.0
print(random.random())
# Random float between two specific values
print(random.uniform(5.0, 10.0))
# Pick one item at random from a list
colors = ["red", "green", "blue", "yellow", "purple"]
print(random.choice(colors))
# Pick multiple items — no repeats
print(random.sample(colors, 3))
# Shuffle a list in place (modifies the original)
cards = [1, 2, 3, 4, 5, 6, 7, 8]
random.shuffle(cards)
print("Shuffled:", cards)
# Set a seed for reproducible results (useful in testing)
random.seed(42)
print("Seeded:", random.randint(1, 100)) # always same value with same seed
random.sample()picks without repetition — useful for lottery draws or selecting test cases.random.seed(42)fixes the random sequence — every run produces the same output. Essential for reproducible tests and machine learning experiments.random.shuffle()modifies the list in place — it does not return a new list.
The datetime Module
The datetime module handles dates, times, and the arithmetic between them. It is used in virtually every application that involves timestamps, scheduling, billing periods, or logging.
from datetime import datetime, date, timedelta
# Current date and time
now = datetime.now()
print("Now :", now)
print("Year :", now.year)
print("Month :", now.month)
print("Weekday :", now.strftime("%A")) # e.g. Monday
# Format a datetime as a readable string
print("Formatted:", now.strftime("%d %B %Y, %H:%M"))
# Today's date only (no time)
today = date.today()
print("Today :", today)
# Create a specific date
launch = date(2024, 1, 15)
print("Launch :", launch)
# Arithmetic — timedelta lets you add or subtract days
in_30_days = today + timedelta(days=30)
print("In 30 days:", in_30_days)
# Difference between two dates
days_since = (today - launch).days
print("Days since launch:", days_since)
strftime()format codes:%A= full weekday,%B= full month name,%d= day,%Y= 4-digit year,%H:%M= hours:minutes.- Subtracting two
dateobjects returns atimedelta. Call.dayson it to get a plain integer. timedeltaalso acceptsweeks,hours,minutes, andsecondsas arguments.
The os Module
The os module lets your Python program interact with the operating system — reading the current folder, listing files, building paths, creating directories, and checking if files exist. Essential for any program that works with the file system.
import os
# Current working directory
print("CWD:", os.getcwd())
# List all files and folders in a directory
print("Contents:", os.listdir("."))
# Check if a file or folder exists
print("Exists:", os.path.exists("data.txt"))
# Build a file path that works on Windows, Mac, and Linux
path = os.path.join("reports", "sales", "q1.csv")
print("Path:", path)
# Extract filename and folder from a path
print("Filename:", os.path.basename(path)) # q1.csv
print("Folder :", os.path.dirname(path)) # reports/sales
# Get the file extension
name, ext = os.path.splitext("report.xlsx")
print("Name:", name, "| Extension:", ext)
# Create a new directory (use exist_ok=True to avoid errors if it exists)
# os.makedirs("new_folder/sub", exist_ok=True)
# Get environment variables
home = os.environ.get("HOME", "Not set")
print("Home directory:", home)
os.path.join()is critical for cross-platform code — never hardcode/or\in paths. Letos.path.join()use the correct separator for the current OS.os.path.splitext()separates a filename from its extension — useful for file type checking and renaming.os.environ.get()reads environment variables safely — the second argument is the default value if the variable is not set.
The sys Module
The sys module gives access to Python interpreter internals — the version running, the module search path, and the ability to exit cleanly. It is used in scripts that need to inspect or control their runtime environment.
import sys
# Python version
print("Version:", sys.version)
print("Version info:", sys.version_info)
# Platform
print("Platform:", sys.platform) # 'linux', 'win32', 'darwin'
# The list of folders Python searches when you write import
print("First 3 search paths:")
for path in sys.path[:3]:
print(" ", path)
# Memory size of a Python object in bytes
data = [i for i in range(1000)]
print("List size:", sys.getsizeof(data), "bytes")
# Exit the program — 0 = success, non-zero = error
# sys.exit(0) ← uncomment to stop execution here
print("sys gives access to the interpreter itself.")
sys.version_info.majorandsys.version_info.minorlet you check the exact version programmatically — useful when your code requires Python 3.9+ or later.sys.getsizeof()returns the memory size of any Python object in bytes — useful for profiling memory-intensive programs.sys.pathis the list of directories Python checks in order when you import a module. You can append to it at runtime to add custom module locations.
The collections Module — Powerful Data Structures
The collections module provides specialised container types that extend Python's built-in list, dict, and tuple. Three of the most useful are Counter, defaultdict, and namedtuple.
from collections import Counter, defaultdict, namedtuple
# Counter — count occurrences in one step
words = ["python", "is", "great", "python", "is", "python", "easy"]
count = Counter(words)
print("Word counts:", count)
print("Most common:", count.most_common(2)) # top 2
# defaultdict — like a dict but never raises KeyError for missing keys
scores = defaultdict(list) # default value is an empty list
scores["Alice"].append(85)
scores["Alice"].append(92)
scores["Bob"].append(78)
print("Scores:", dict(scores))
# namedtuple — like a tuple but fields have names
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 7)
print("Point:", p)
print("X:", p.x, "Y:", p.y)
Employee = namedtuple("Employee", ["name", "dept", "salary"])
emp = Employee("Priya", "Engineering", 90000)
print(emp.name, "in", emp.dept, "earns", emp.salary)
Counterreplaces the manual word-counting loop you wrote in Lesson 15 — in one line.defaultdicteliminates the "check if key exists, then initialise" pattern — it creates the default value automatically on first access.namedtupleis a memory-efficient way to represent structured records — lighter than a full class, more readable than a plain tuple.
The itertools Module — Efficient Iteration
The itertools module provides tools for working with iterators in a memory-efficient way. It is used in data processing, combinatorics, and anywhere you need to generate sequences without building them all in memory at once.
import itertools
# chain — combine multiple iterables into one sequence
combined = list(itertools.chain([1, 2, 3], [4, 5], [6]))
print("Chained:", combined)
# product — cartesian product (like nested loops)
sizes = ["S", "M", "L"]
colors = ["Red", "Blue"]
combos = list(itertools.product(sizes, colors))
print("Size-color combinations:", combos)
# permutations — all ordered arrangements
letters = list(itertools.permutations("ABC", 2))
print("2-char permutations of ABC:", letters)
# combinations — all unordered selections (no repeats)
team = list(itertools.combinations(["Alice", "Bob", "Carol", "Dan"], 2))
print("Possible pairs:", team)
# islice — take only the first N items from any iterator (memory-efficient)
evens = itertools.count(0, 2) # infinite even numbers
first_5_evens = list(itertools.islice(evens, 5))
print("First 5 evens:", first_5_evens)
itertools.product()is the clean way to generate all combinations — replaces nested loops.itertools.count()creates an infinite sequence —islice()takes only as many as you need without crashing.combinationsvspermutations: combinations ignore order (AB = BA), permutations treat order as significant (AB ≠ BA).
Creating Your Own Module
Any .py file you create is immediately importable as a module. Put reusable helper functions in a separate file and import them wherever you need them — this is how large Python projects are structured.
# === FILE: mytools.py ===
# Save this as mytools.py in the same folder as your main script
def greet(name):
"""Return a welcome message."""
return f"Hello, {name}! Welcome to Dataplexa."
def calc_tax(price, rate=8):
"""Returns tax amount. Default rate is 8%."""
return round(price * rate / 100, 2)
def is_even(n):
"""Returns True if n is even."""
return n % 2 == 0
def word_count(text):
"""Returns a dict of word frequencies."""
words = text.lower().split()
counts = {}
for w in words:
counts[w] = counts.get(w, 0) + 1
return counts
TAX_RATE = 8 # module-level constant
# === FILE: main.py ===
import mytools
print(mytools.greet("Arjun"))
print("Tax on Rs.500:", mytools.calc_tax(500))
print("Is 9 even?", mytools.is_even(9))
print("Tax rate:", mytools.TAX_RATE, "%")
freq = mytools.word_count("python is great and python is easy")
print("Word freq:", freq)
- Any file in the same folder can be imported by name — Python finds it automatically.
- Module-level variables like
TAX_RATEare also importable — they act like constants shared across the project. - You can also import specific items:
from mytools import greet, calc_tax.
What is a Package — and How to Structure One
A package is a folder of modules grouped together under one name. A folder becomes a Python package when it contains a file called __init__.py. This is how all serious Python projects are organised — instead of one huge file, code lives in focused modules inside a structured folder tree.
# Real-world project structure using packages:
#
# myapp/
# ├── __init__.py ← makes myapp a package
# ├── billing.py ← invoice and payment functions
# ├── users.py ← user management functions
# ├── reports/ ← sub-package for reports
# │ ├── __init__.py
# │ ├── sales.py
# │ └── analytics.py
# └── utils/
# ├── __init__.py
# └── helpers.py
#
# How you import from a package:
# import myapp.billing
# from myapp.billing import calc_invoice
# from myapp.reports.sales import monthly_report
#
# Real example — os is a package, path is a sub-module:
import os.path
print(os.path.join("data", "2025", "sales.csv"))
print(os.path.exists("/tmp"))
# datetime is also a package — datetime.datetime is a class inside it:
from datetime import datetime
print(datetime.now().strftime("%Y-%m-%d"))
- The
__init__.pyfile can be completely empty — its presence alone tells Python the folder is a package. - You have been using packages all along —
os.pathis a sub-module inside theospackage. - Sub-packages allow deep nesting —
from myapp.reports.analytics import trend_reportis perfectly valid.
Installing Third-Party Packages with pip
Python's Standard Library is powerful, but the real ecosystem comes from thousands of free community-built packages on PyPI (Python Package Index). You install them using pip — run these commands in your terminal, not inside a Python script.
# Run in your terminal (not inside Python):
# Install a package
# pip install requests ← HTTP requests / web APIs
# pip install pandas ← data analysis
# pip install numpy ← numerical computing
# pip install flask ← web framework
# pip install matplotlib ← charts and graphs
# Install a specific version
# pip install requests==2.28.0
# Install multiple from a requirements file
# pip install -r requirements.txt
# See all installed packages
# pip list
# See details about one package
# pip show requests
# Uninstall a package
# pip uninstall requests
# Create a requirements.txt of everything in your environment
# pip freeze > requirements.txt
# After installing, import just like a built-in module:
# import requests
# import pandas as pd
# import numpy as np
print("pip installs packages from PyPI — the Python Package Index.")
pip freeze > requirements.txtcaptures every installed package with its version — share this file so others can recreate your exact environment withpip install -r requirements.txt.- Always use virtual environments (Lesson 29) to isolate project dependencies — packages installed for one project should not affect another.
How Python Finds Modules — The Search Path
When you write import something, Python searches for the module in a specific order. Understanding this order helps you diagnose ModuleNotFoundError and place your own modules correctly.
import sys
# Python searches these locations in order:
# 1. The current script's directory
# 2. Directories in the PYTHONPATH environment variable
# 3. The Standard Library directories
# 4. Site-packages (where pip installs third-party packages)
print("Python searches these paths:")
for i, path in enumerate(sys.path, 1):
print(f" {i}. {path}")
# You can add a custom path at runtime:
# sys.path.append("/my/custom/modules")
# Now Python will also search that folder
# Find where a module is physically located on disk
import os
print("\nmath module location:", os.path.abspath(os.__file__))
- If Python cannot find your module, the most common reason is that the file is in a different folder than expected — check
sys.pathto see where Python is looking. module.__file__tells you exactly where a module lives on disk — useful for debugging import issues.
The if __name__ == "__main__" Guard
When Python runs a file directly, it sets __name__ to "__main__". When the same file is imported as a module, __name__ is set to the file's own name. This guard lets you write test or demo code that only runs when you execute the file directly — not when another file imports your functions.
def add(a, b):
return a + b
def multiply(a, b):
return a * b
def greet(name):
return f"Hi, {name}!"
# This block ONLY runs when you execute this file directly:
# python myfile.py
#
# It does NOT run when another file imports this module:
# import myfile ← __name__ is "myfile" here, not "__main__"
if __name__ == "__main__":
print("Running as a standalone script")
print("add(10, 20) =", add(10, 20))
print("multiply(3, 7) =", multiply(3, 7))
print("greet('Priya') =", greet("Priya"))
- This pattern is in almost every professional Python script — it separates reusable library functions from run-once startup code.
- Without it, every time you import a module, any top-level code (print statements, initialisation, database connections) would also run — which is rarely what you want.
- Put your main program logic, tests, and demos inside this guard. Put pure functions and classes outside it.
Built-in Standard Library — Quick Reference
| Module | Key Items | Used For |
|---|---|---|
math | sqrt, floor, ceil, pi, factorial | Mathematical operations |
random | randint, choice, sample, shuffle, seed | Random values and selections |
datetime | datetime.now(), date.today(), timedelta | Dates, times, and arithmetic |
os | getcwd, listdir, path.join, makedirs | File system operations |
sys | version, path, exit, getsizeof | Interpreter information |
collections | Counter, defaultdict, namedtuple | Specialised data structures |
itertools | chain, product, combinations, islice | Efficient iteration and combinatorics |
json | loads, dumps, load, dump | Read and write JSON data |
re | search, match, findall, sub | Regular expressions (Lesson 24) |
functools | reduce, lru_cache, partial | Functional programming utilities |
Practice
What keyword loads a module into your program?
What keyword gives a module a shorter nickname when importing?
What tool do you use in the terminal to install third-party Python packages?
What file must exist in a folder to make Python treat it as a package?
What value does __name__ have when a Python file is run directly?
Which class from the collections module counts item occurrences automatically?
Quick Quiz
Which import lets you use sqrt() directly without a prefix?
Which function picks one random item from a list?
Which function builds a file path that works on all operating systems?
What happens to code inside if __name__ == "__main__": when the file is imported?
Which correctly calculates 5! using the math module?
Which itertools function returns all unordered selections from a collection?