Modules and Packages


What are Modules?

In Python, a module is simply a file containing Python definitions and statements. Think of it as a toolbox filled with functions, classes, and variables that you can use in other Python programs. When you create a .py file, you're essentially creating a module.

Why use modules?

  • Organization: Modules help you logically organize your code into separate, manageable files. Instead of one giant script, you can break your program into smaller, focused modules.
  • Reusability: Once you've written a useful function or class in a module, you can reuse it across multiple projects without rewriting the code. This saves time and reduces errors.
  • Namespace Isolation: Each module has its own namespace, preventing name clashes between different parts of your program.

Creating Your Own Modules

Creating your own modules is straightforward. All you need to do is save your Python code in a file with a .py extension.

Let's walk through some examples, from beginner-friendly to more advanced scenarios.

Example 1: Basic Arithmetic Module (Beginner)

This module will contain simple functions for addition and subtraction.

 

 

# filename: my_math.py

# A simple module demonstrating basic arithmetic operations.

def add(a, b):
    """
    Adds two numbers and returns the sum.
    Parameters:
        a (int/float): The first number.
        b (int/float): The second number.
    Returns:
        int/float: The sum of a and b.
    """
    return a + b

def subtract(a, b):
    """
    Subtracts the second number from the first and returns the difference.
    Parameters:
        a (int/float): The number to subtract from.
        b (int/float): The number to subtract.
    Returns:
        int/float: The difference between a and b.
    """
    return a - b

print("my_math module loaded!") # This will print when the module is imported

 

Example 2: String Utility Module (Beginner to Intermediate)

This module will offer functions to manipulate strings.

 

# filename: string_utils.py

# This module provides utility functions for string manipulation.

def reverse_string(s):
    """
    Reverses a given string.
    Parameters:
        s (str): The input string.
    Returns:
        str: The reversed string.
    """
    return s[::-1]

def is_palindrome(s):
    """
    Checks if a string is a palindrome (reads the same forwards and backwards).
    Parameters:
        s (str): The input string.
    Returns:
        bool: True if the string is a palindrome, False otherwise.
    """
    cleaned_s = "".join(filter(str.isalnum, s)).lower() # Remove non-alphanumeric and convert to lowercase
    return cleaned_s == reverse_string(cleaned_s)

def count_vowels(s):
    """
    Counts the number of vowels in a string.
    Parameters:
        s (str): The input string.
    Returns:
        int: The count of vowels.
    """
    vowels = "aeiouAEIOU"
    return sum(1 for char in s if char in vowels)

if __name__ == "__main__":
    # This block only runs when the script is executed directly, not when imported as a module.
    print("Testing string_utils module:")
    print(f"Reversed 'hello': {reverse_string('hello')}")
    print(f"'madam' is palindrome: {is_palindrome('madam')}")
    print(f"'Python' is palindrome: {is_palindrome('Python')}")
    print(f"Vowels in 'Programming': {count_vowels('Programming')}")

 

Example 3: Simple Class Module (Intermediate)

This module defines a basic Person class.

 

# filename: person.py

# This module defines a simple Person class.

class Person:
    """
    A class to represent a person with a name and age.
    """
    def __init__(self, name, age):
        """
        Initializes a new Person object.
        Parameters:
            name (str): The name of the person.
            age (int): The age of the person.
        """
        self.name = name
        self.age = age

    def introduce(self):
        """
        Returns a greeting message including the person's name and age.
        """
        return f"Hi, my name is {self.name} and I am {self.age} years old."

    def have_birthday(self):
        """
        Increments the person's age by one.
        """
        self.age += 1
        print(f"{self.name} just had a birthday and is now {self.age} years old!")

# Example of how to use the Person class if run directly
if __name__ == "__main__":
    john = Person("John Doe", 30)
    print(john.introduce())
    john.have_birthday()
    print(f"New age: {john.age}")

 

Example 4: Configuration Module (Intermediate to Advanced)

This module could hold application-wide configuration settings.

 

# filename: config.py

# This module stores application configuration settings.

# Database settings
DATABASE_URL = "sqlite:///my_application.db"
API_KEY = "your_secret_api_key_12345" # In a real app, this would be loaded from env vars

# Application settings
DEBUG_MODE = True
MAX_CONNECTIONS = 10
LOG_LEVEL = "INFO"

def get_db_connection_string():
    """
    Returns the database connection URL.
    """
    return DATABASE_URL

def set_debug_mode(mode):
    """
    Sets the debug mode for the application.
    Parameters:
        mode (bool): True for debug mode, False otherwise.
    """
    global DEBUG_MODE # Use global to modify the module-level variable
    DEBUG_MODE = mode
    print(f"Debug mode set to: {DEBUG_MODE}")

if __name__ == "__main__":
    print(f"Current DB URL: {get_db_connection_string()}")
    print(f"Initial Debug Mode: {DEBUG_MODE}")
    set_debug_mode(False)
    print(f"Updated Debug Mode: {DEBUG_MODE}")

 

Example 5: Advanced Data Processing Module (Advanced)

This module might contain more complex data structures or algorithms.

 

# filename: data_processor.py

# This module provides functions for advanced data processing.
import collections

def calculate_word_frequency(text):
    """
    Calculates the frequency of each word in a given text.
    Converts text to lowercase and removes punctuation before counting.
    Parameters:
        text (str): The input text.
    Returns:
        collections.Counter: A Counter object mapping words to their frequencies.
    """
    # Remove punctuation and convert to lowercase
    cleaned_text = ''.join(char.lower() if char.isalnum() or char.isspace() else ' ' for char in text)
    words = cleaned_text.split()
    return collections.Counter(words)

def flatten_list_of_lists(list_of_lists):
    """
    Flattens a list of lists into a single list.
    Parameters:
        list_of_lists (list): A list where each element is another list.
    Returns:
        list: A single flattened list.
    """
    return [item for sublist in list_of_lists for item in sublist]

def unique_elements_preserving_order(input_list):
    """
    Returns a list of unique elements from the input list, preserving their original order.
    Parameters:
        input_list (list): The list to process.
    Returns:
        list: A list with unique elements in their original order.
    """
    seen = set()
    result = []
    for item in input_list:
        if item not in seen:
            seen.add(item)
            result.append(item)
    return result

if __name__ == "__main__":
    sample_text = "Hello world, this is a sample text. Hello again!"
    word_freq = calculate_word_frequency(sample_text)
    print(f"Word frequencies: {word_freq}")

    nested_list = [[1, 2], [3, 4, 5], [6]]
    flattened = flatten_list_of_lists(nested_list)
    print(f"Flattened list: {flattened}")

    ordered_list = [1, 2, 2, 3, 1, 4, 5, 3]
    unique_ordered = unique_elements_preserving_order(ordered_list)
    print(f"Unique elements (preserving order): {unique_ordered}")

 

Importing Modules

Once you've created a module, you'll want to use its contents in other Python scripts. This is where the import statement comes in. Python provides several ways to import modules, each with its own advantages.

import module_name

This is the most common way to import a module. It imports the entire module, and you access its contents using the dot notation (module_name.function_name).

Example 1: Importing my_math (Beginner)

 

# filename: main_program_1.py

# Demonstrates importing the entire 'my_math' module.
import my_math # Imports the my_math.py file as a module.

# Use functions from the my_math module by prefixing with 'my_math.'
result_add = my_math.add(10, 5)
print(f"10 + 5 = {result_add}")

result_subtract = my_math.subtract(100, 30)
print(f"100 - 30 = {result_subtract}")

 

Example 2: Importing string_utils (Beginner to Intermediate)

 

# filename: main_program_2.py

# Demonstrates importing the entire 'string_utils' module.
import string_utils # Imports the string_utils.py file.

text = "Racecar"
reversed_text = string_utils.reverse_string(text)
print(f"'{text}' reversed is '{reversed_text}'")

is_racecar_palindrome = string_utils.is_palindrome(text)
print(f"Is '{text}' a palindrome? {is_racecar_palindrome}")

sentence = "Python is a fantastic language!"
vowel_count = string_utils.count_vowels(sentence)
print(f"Number of vowels in '{sentence}': {vowel_count}")

 

Example 3: Importing person (Intermediate)

 

# filename: main_program_3.py

# Demonstrates importing the entire 'person' module.
import person # Imports the person.py file.

# Create an instance of the Person class from the imported module.
alice = person.Person("Alice Smith", 25)
print(alice.introduce())

alice.have_birthday()
print(f"Alice's new age: {alice.age}")

bob = person.Person("Bob Johnson", 40)
print(bob.introduce())

 

Example 4: Importing config (Intermediate to Advanced)

 

# filename: main_program_4.py

# Demonstrates importing the entire 'config' module.
import config # Imports the config.py file.

# Access module-level variables directly
print(f"Database URL from config: {config.DATABASE_URL}")
print(f"Is debug mode enabled? {config.DEBUG_MODE}")

# Call functions defined in the config module
db_string = config.get_db_connection_string()
print(f"Database connection string via function: {db_string}")

# Change a setting via the module's function
config.set_debug_mode(False)
print(f"Debug mode after change: {config.DEBUG_MODE}")

 

Example 5: Importing data_processor (Advanced)

 

# filename: main_program_5.py

# Demonstrates importing the entire 'data_processor' module.
import data_processor # Imports the data_processor.py file.

long_text = "The quick brown fox jumps over the lazy dog. The fox is quick."
freq = data_processor.calculate_word_frequency(long_text)
print(f"Word frequencies in long text: {freq}")
print(f"Frequency of 'the': {freq['the']}")

matrix = [[10, 20], [30], [40, 50, 60]]
flat_matrix = data_processor.flatten_list_of_lists(matrix)
print(f"Flattened matrix: {flat_matrix}")

data_series = ['apple', 'banana', 'apple', 'orange', 'banana', 'grape']
unique_ordered = data_processor.unique_elements_preserving_order(data_series)
print(f"Unique elements (ordered): {unique_ordered}")

 

from module_name import name1, name2, ...

This statement allows you to import specific names (functions, classes, variables) directly from a module into your current namespace. This means you don't need to use the module_name. prefix.

Example 1: Importing add and subtract from my_math (Beginner)

 

# filename: selective_import_1.py

# Demonstrates importing specific functions from 'my_math'.
from my_math import add, subtract # Imports only add and subtract functions.

# Use functions directly without 'my_math.' prefix
print(f"5 + 3 = {add(5, 3)}")
print(f"20 - 7 = {subtract(20, 7)}")

# This would cause an error because 'my_math' itself is not imported, only its contents
# print(my_math.add(1, 2))

 

Example 2: Importing is_palindrome and reverse_string from string_utils (Beginner to Intermediate)

 

# filename: selective_import_2.py

# Demonstrates importing specific functions from 'string_utils'.
from string_utils import is_palindrome, reverse_string # Imports only these two functions.

word1 = "level"
word2 = "hello"

print(f"Is '{word1}' a palindrome? {is_palindrome(word1)}")
print(f"Is '{word2}' a palindrome? {is_palindrome(word2)}")
print(f"Reversed '{word2}': {reverse_string(word2)}")

 

 

Example 3: Importing Person class from person (Intermediate)

 

# filename: selective_import_3.py

# Demonstrates importing the 'Person' class from 'person'.
from person import Person # Imports only the Person class.

# Create an instance of Person directly
charlie = Person("Charlie Brown", 8)
print(charlie.introduce())

sally = Person("Sally Brown", 6)
sally.have_birthday()

 

 

Example 4: Importing DATABASE_URL and set_debug_mode from config (Intermediate to Advanced)

 

# filename: selective_import_4.py

# Demonstrates importing specific items from 'config'.
from config import DATABASE_URL, set_debug_mode, DEBUG_MODE # Imports these specific names.

# Access DATABASE_URL directly
print(f"The configured database URL is: {DATABASE_URL}")

# Call set_debug_mode directly
set_debug_mode(True)

# Access DEBUG_MODE directly
print(f"Current debug mode status: {DEBUG_MODE}")

 

 

Example 5: Importing calculate_word_frequency from data_processor (Advanced)

 

# filename: selective_import_5.py

# Demonstrates importing a specific function from 'data_processor'.
from data_processor import calculate_word_frequency # Imports only this function.

article_text = "Data science is a fascinating field. Data is everywhere."
frequencies = calculate_word_frequency(article_text)
print(f"Word frequencies in article: {frequencies}")
print(f"Frequency of 'data': {frequencies['data']}")

 

 

from module_name import * (Wildcard Import)

This imports all public names from a module into the current namespace. While convenient, it's generally discouraged in production code because it can lead to name clashes and make it difficult to determine where a specific name originated.

Example 1: Wildcard import of my_math (Beginner - Use with caution!)

 

# filename: wildcard_import_1.py

# Demonstrates a wildcard import from 'my_math'.
from my_math import * # Imports all public names from my_math.

# Now you can use add() and subtract() directly.
print(f"7 + 2 = {add(7, 2)}")
print(f"15 - 8 = {subtract(15, 8)}")

 

Example 2: Wildcard import of string_utils (Beginner to Intermediate - Use with caution!)

 

# filename: wildcard_import_2.py

# Demonstrates a wildcard import from 'string_utils'.
from string_utils import *

text_phrase = "A man, a plan, a canal: Panama"
print(f"Is '{text_phrase}' a palindrome? {is_palindrome(text_phrase)}")
print(f"Reversed '{text_phrase}': {reverse_string(text_phrase)}")
print(f"Vowels in '{text_phrase}': {count_vowels(text_phrase)}")

 

 

import module_name as alias

This allows you to give a module a different, shorter, or more convenient name (an alias) when you import it. This is useful for long module names or to avoid name clashes with existing variables.

Example 1: Aliasing my_math as mm (Beginner)

 

# filename: aliased_import_1.py

# Demonstrates aliasing the 'my_math' module as 'mm'.
import my_math as mm # Imports my_math and gives it the alias 'mm'.

# Now use 'mm.' to access functions.
print(f"8 + 4 = {mm.add(8, 4)}")
print(f"50 - 25 = {mm.subtract(50, 25)}")

 

 

Example 2: Aliasing string_utils as su (Beginner to Intermediate)

 

# filename: aliased_import_2.py

# Demonstrates aliasing the 'string_utils' module as 'su'.
import string_utils as su

my_string = "Pythonic"
print(f"Reversed using alias: {su.reverse_string(my_string)}")

 

 

Example 3: Aliasing Person as Individual (Intermediate)

 

# filename: aliased_import_3.py

# Demonstrates aliasing the 'Person' class from 'person'.
from person import Person as Individual # Imports Person and aliases it as Individual.

# Now create instances using the alias.
teacher = Individual("Ms. Davis", 45)
student = Individual("Tom Petty", 16)

print(teacher.introduce())
print(student.introduce())

 

 

Example 4: Aliasing get_db_connection_string as get_db_uri (Intermediate to Advanced)

 

# filename: aliased_import_4.py

# Demonstrates aliasing a function from 'config'.
from config import get_db_connection_string as get_db_uri # Alias the function name.

# Use the aliased function name.
uri = get_db_uri()
print(f"Database URI obtained via alias: {uri}")

 

 

Example 5: Aliasing calculate_word_frequency as word_count (Advanced)

 

# filename: aliased_import_5.py

# Demonstrates aliasing a function from 'data_processor'.
from data_processor import calculate_word_frequency as word_count

sample_doc = "Learning Python is fun. Python is powerful."
counts = word_count(sample_doc)
print(f"Word counts using alias: {counts}")

 

 

The dir() and help() Functions

Python provides built-in functions that are incredibly useful for exploring modules and understanding their contents: dir() and help().

dir() Function

The dir() function returns a list of names (attributes, functions, classes, variables) in the current scope, or for an object/module if specified. It's great for quickly seeing what's available.

Example 1: Using dir() on my_math (Beginner)

 

# filename: dir_help_1.py

import my_math

# Use dir() to see what's inside the my_math module.
print("Contents of my_math module:")
print(dir(my_math)) # This will list 'add', 'subtract', and other internal attributes.

# You can also use dir() without arguments to see current scope
print("\nContents of current scope:")
print(dir())

 

 

Example 2: Using dir() on a specific imported function (Beginner to Intermediate)

 

# filename: dir_help_2.py

from string_utils import reverse_string

# dir() on a function object itself
print("Attributes of reverse_string function:")
print(dir(reverse_string)) # Shows methods and attributes of the function object.

 

 

Example 3: Using dir() on a class instance (Intermediate)

 

# filename: dir_help_3.py

from person import Person

john = Person("John Doe", 30)

# dir() on an instance will show its attributes and methods
print("Attributes and methods of John Doe object:")
print(dir(john)) # Shows 'name', 'age', 'introduce', 'have_birthday', and inherited methods.

 

 

Example 4: Using dir() on a built-in module (math) (Intermediate to Advanced)

 

# filename: dir_help_4.py

import math

# Explore the contents of the built-in 'math' module
print("Contents of the built-in 'math' module:")
print(dir(math)) # Will show functions like 'sqrt', 'sin', 'cos', 'pi', etc.

 

 

Example 5: Using dir() on a string object (Advanced)

 

# filename: dir_help_5.py

my_string = "hello"

# dir() on a string object reveals all its available methods
print("Methods and attributes of a string object:")
print(dir(my_string)) # Shows methods like 'upper', 'lower', 'split', 'replace', etc.

 

 

help() Function

The help() function is incredibly powerful for getting detailed documentation (docstrings) about modules, functions, classes, and keywords. It's your go-to for understanding how something works without leaving your Python interpreter.

Example 1: Getting help on my_math module (Beginner)

 

# filename: dir_help_6.py

import my_math

# Get help on the entire my_math module. This will display module-level docstring
# and docstrings for its functions.
print("--- Help for my_math module ---")
help(my_math)

 

 

Example 2: Getting help on add function from my_math (Beginner to Intermediate)

 

# filename: dir_help_7.py

import my_math

# Get help on a specific function within the module.
print("--- Help for my_math.add function ---")
help(my_math.add)

 

Example 3: Getting help on Person class (Intermediate)

 

# filename: dir_help_8.py

from person import Person

# Get help on the Person class.
print("--- Help for Person class ---")
help(Person)

# You can also get help on an instance's method
print("\n--- Help for Person.introduce method ---")
help(Person.introduce)

 

 

Example 4: Getting help on built-in random module (Intermediate to Advanced)

 

# filename: dir_help_9.py

import random

# Explore the random module's documentation.
print("--- Help for built-in random module ---")
help(random)

 

 

Example 5: Getting help on a specific function from a built-in module (os.path.exists) (Advanced)

 

# filename: dir_help_10.py

import os.path # Import the os.path submodule

# Get detailed help on the os.path.exists function.
print("--- Help for os.path.exists function ---")
help(os.path.exists)

 

 

Standard Library Modules

Python comes with a vast collection of pre-installed modules known as the Standard Library. These modules provide ready-to-use functionalities for a wide range of tasks, from mathematical operations to file handling, networking, and data serialization. Utilizing the standard library is crucial for efficient and robust Python development.

Let's introduce some commonly used standard library modules.

math Module

The math module provides access to mathematical functions and constants.

Example 1: Basic Math Operations (Beginner)

 

# filename: std_lib_math_1.py

import math

print(f"Value of PI: {math.pi}")
print(f"Value of E: {math.e}")

# Square root
print(f"Square root of 16: {math.sqrt(16)}")

# Power
print(f"2 raised to the power of 3: {math.pow(2, 3)}") # Equivalent to 2**3

 

 

Example 2: Trigonometric Functions (Beginner to Intermediate)

 

# filename: std_lib_math_2.py

import math

# Sine and Cosine (angles in radians)
angle_degrees = 90
angle_radians = math.radians(angle_degrees) # Convert degrees to radians

print(f"Sine of 90 degrees: {math.sin(angle_radians)}")
print(f"Cosine of 90 degrees: {math.cos(angle_radians)}")

# Factorial
print(f"Factorial of 5: {math.factorial(5)}") # 5 * 4 * 3 * 2 * 1

 

 

Example 3: Ceiling and Floor (Intermediate)

 

# filename: std_lib_math_3.py

import math

# math.ceil() returns the smallest integer greater than or equal to x.
print(f"Ceiling of 4.2: {math.ceil(4.2)}")
print(f"Ceiling of 4.8: {math.ceil(4.8)}")
print(f"Ceiling of 4.0: {math.ceil(4.0)}")

# math.floor() returns the largest integer less than or equal to x.
print(f"Floor of 4.2: {math.floor(4.2)}")
print(f"Floor of 4.8: {math.floor(4.8)}")
print(f"Floor of 4.0: {math.floor(4.0)}")

 

 

Example 4: Logarithmic Functions (Intermediate to Advanced)

 

# filename: std_lib_math_4.py

import math

# Natural logarithm (base e)
print(f"Natural logarithm of 10: {math.log(10)}")

# Logarithm with a specific base
print(f"Logarithm of 100 with base 10: {math.log(100, 10)}")

# Logarithm base 2
print(f"Logarithm of 8 with base 2: {math.log2(8)}")

 

Example 5: Infinity and NaN (Advanced)

 

# filename: std_lib_math_5.py

import math

# Positive infinity
print(f"Positive infinity: {math.inf}")

# Negative infinity
print(f"Negative infinity: {-math.inf}")

# Not a Number (NaN)
print(f"Not a Number: {math.nan}")

# Checking for infinity or NaN
print(f"Is math.inf infinite? {math.isinf(math.inf)}")
print(f"Is math.nan a NaN? {math.isnan(math.nan)}")

 

 

random Module

The random module implements pseudo-random number generators for various distributions. It's useful for simulations, games, and security (though for cryptographic security, use secrets module).

Example 1: Generating Random Integers (Beginner)

 

# filename: std_lib_random_1.py

import random

# Generate a random integer between 1 and 10 (inclusive)
random_int = random.randint(1, 10)
print(f"Random integer between 1 and 10: {random_int}")

# Generate a random float between 0.0 (inclusive) and 1.0 (exclusive)
random_float = random.random()
print(f"Random float between 0.0 and 1.0: {random_float}")

 

 

Example 2: Choosing from a Sequence (Beginner to Intermediate)

 

# filename: std_lib_random_2.py

import random

my_list = ['apple', 'banana', 'cherry', 'date']

# Choose a random element from a list
random_fruit = random.choice(my_list)
print(f"Random fruit: {random_fruit}")

# Choose multiple unique elements from a list without replacement
random_fruits = random.sample(my_list, 2)
print(f"Two random unique fruits: {random_fruits}")

 

 

Example 3: Shuffling a List (Intermediate)

 

# filename: std_lib_random_3.py

import random

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]

print(f"Original list: {numbers}")
random.shuffle(numbers) # Shuffles the list in place
print(f"Shuffled list: {numbers}")

deck_of_cards = ["Ace", "King", "Queen", "Jack", "10", "9"]
random.shuffle(deck_of_cards)
print(f"Shuffled deck: {deck_of_cards}")

 

Example 4: Generating Random Floats within a Range (Intermediate to Advanced)

 

# filename: std_lib_random_4.py

import random

# Generate a random float between a and b (inclusive)
random_float_range = random.uniform(10.5, 20.5)
print(f"Random float between 10.5 and 20.5: {random_float_range}")

# Generate a random integer from range(start, stop, step)
# This is like random.randrange(1, 10, 2) which would give 1, 3, 5, 7, 9
random_even = random.randrange(2, 11, 2)
print(f"Random even number between 2 and 10: {random_even}")

 

 

Example 5: Seeding the Random Number Generator (Advanced)

 

# filename: std_lib_random_5.py

import random

# Seeding ensures reproducibility of random sequences.
# If you use the same seed, you'll get the same "random" numbers.

print("First sequence with seed 42:")
random.seed(42)
print([random.randint(1, 100) for _ in range(5)])

print("Second sequence with seed 42 (same as first):")
random.seed(42)
print([random.randint(1, 100) for _ in range(5)])

print("Third sequence with no seed (different):")
print([random.randint(1, 100) for _ in range(5)])

 

 

datetime Module

The datetime module supplies classes for manipulating dates and times in both simple and complex ways.

Example 1: Getting Current Date and Time (Beginner)

 

# filename: std_lib_datetime_1.py

import datetime

# Get the current date and time
now = datetime.datetime.now()
print(f"Current date and time: {now}")

# Get just the current date
today = datetime.date.today()
print(f"Current date: {today}")

# Get just the current time
current_time = datetime.datetime.now().time()
print(f"Current time: {current_time}")

 

 

Example 2: Creating Specific Dates and Times (Beginner to Intermediate)

 

# filename: std_lib_datetime_2.py

import datetime

# Create a specific date
my_birthday = datetime.date(1990, 5, 15)
print(f"My birthday: {my_birthday}")

# Create a specific time
meeting_time = datetime.time(9, 30, 0) # 9:30 AM
print(f"Meeting time: {meeting_time}")

# Create a specific datetime object
event_datetime = datetime.datetime(2025, 12, 25, 18, 0, 0) # Dec 25, 2025, 6:00 PM
print(f"Christmas event: {event_datetime}")

 

Example 3: Formatting Dates and Times (Intermediate)

 

# filename: std_lib_datetime_3.py

import datetime

now = datetime.datetime.now()

# Format date and time using strftime()
# %Y: Year with century (e.g., 2025)
# %m: Month as a zero-padded decimal number (e.g., 06)
# %d: Day of the month as a zero-padded decimal number (e.g., 13)
# %H: Hour (24-hour clock) as a zero-padded decimal number (e.g., 13)
# %M: Minute as a zero-padded decimal number (e.g., 19)
# %S: Second as a zero-padded decimal number (e.g., 53)
formatted_date_time = now.strftime("%Y-%m-%d %H:%M:%S")
print(f"Formatted date and time: {formatted_date_time}")

# Custom format for a date
custom_date_format = now.strftime("Today is %A, %B %d, %Y") # e.g., "Today is Friday, June 13, 2025"
print(f"Custom date format: {custom_date_format}")

 

Example 4: Date and Time Arithmetic (Intermediate to Advanced)

 

# filename: std_lib_datetime_4.py

import datetime

today = datetime.date.today()
print(f"Today: {today}")

# Create a timedelta object
one_day = datetime.timedelta(days=1)
one_week = datetime.timedelta(weeks=1)
one_hour = datetime.timedelta(hours=1)

# Add/Subtract time
tomorrow = today + one_day
print(f"Tomorrow: {tomorrow}")

last_week = today - one_week
print(f"Last week: {last_week}")

# Calculate difference between two datetimes
future_date = datetime.datetime(2026, 1, 1)
time_to_new_year = future_date - datetime.datetime.now()
print(f"Time until New Year 2026: {time_to_new_year}")
print(f"Days left: {time_to_new_year.days}")

 

 

Example 5: Parsing Dates from Strings (strptime) (Advanced)

 

# filename: std_lib_datetime_5.py

import datetime

date_string_1 = "2023-10-26"
date_object_1 = datetime.datetime.strptime(date_string_1, "%Y-%m-%d").date()
print(f"Parsed date 1: {date_object_1}, Type: {type(date_object_1)}")

date_string_2 = "March 15, 2024 14:30:00"
date_object_2 = datetime.datetime.strptime(date_string_2, "%B %d, %Y %H:%M:%S")
print(f"Parsed datetime 2: {date_object_2}, Type: {type(date_object_2)}")

# Handling different date formats
date_string_3 = "12/25/2025"
date_object_3 = datetime.datetime.strptime(date_string_3, "%m/%d/%Y").date()
print(f"Parsed date 3: {date_object_3}")

 

 

os Module

The os module provides a way of using operating system dependent functionality, such as reading or writing to the file system,1 managing directories, and interacting with environment variables.

Example 1: Interacting with Directories (Beginner)

 

# filename: std_lib_os_1.py

import os

# Get the current working directory
current_dir = os.getcwd()
print(f"Current working directory: {current_dir}")

# List contents of a directory
print("\nContents of current directory:")
for item in os.listdir(current_dir):
    print(item)

# Create a new directory
new_dir_name = "my_new_directory"
if not os.path.exists(new_dir_name):
    os.mkdir(new_dir_name)
    print(f"\nCreated directory: {new_dir_name}")
else:
    print(f"\nDirectory '{new_dir_name}' already exists.")

 

 

Example 2: Path Manipulation (os.path) (Beginner to Intermediate)

 

# filename: std_lib_os_2.py

import os

# Joining path components safely (handles slashes/backslashes correctly)
path_join = os.path.join("my_folder", "sub_folder", "my_file.txt")
print(f"Joined path: {path_join}")

# Getting base name and directory name
full_path = "/home/user/documents/report.pdf"
dirname = os.path.dirname(full_path)
basename = os.path.basename(full_path)
print(f"Directory name: {dirname}")
print(f"Base name: {basename}")

# Checking if a path exists
print(f"Does '{full_path}' exist? {os.path.exists(full_path)}")
print(f"Does 'std_lib_os_1.py' exist? {os.path.exists('std_lib_os_1.py')}")

 

 

Example 3: Renaming and Deleting Files/Directories (Intermediate)

 

# filename: std_lib_os_3.py

import os

# Create a dummy file for demonstration
with open("old_name.txt", "w") as f:
    f.write("This is a test file.")

if os.path.exists("old_name.txt"):
    print("old_name.txt created.")
    # Rename a file
    os.rename("old_name.txt", "new_name.txt")
    print("File renamed from old_name.txt to new_name.txt")

# Clean up: remove the renamed file
if os.path.exists("new_name.txt"):
    os.remove("new_name.txt")
    print("new_name.txt removed.")

# Removing an empty directory (must be empty!)
empty_dir_to_remove = "empty_test_dir"
if not os.path.exists(empty_dir_to_remove):
    os.mkdir(empty_dir_to_remove)
    print(f"Created '{empty_dir_to_remove}' for removal test.")
    os.rmdir(empty_dir_to_remove)
    print(f"Removed empty directory: {empty_dir_to_remove}")
else:
    print(f"Directory '{empty_dir_to_remove}' already exists; skipping creation/removal.")

 

 

Example 4: Environment Variables (Intermediate to Advanced)

 

# filename: std_lib_os_4.py

import os

# Get an environment variable
# os.environ is a dictionary-like object representing the user's environment variables.
# Using .get() is safer as it returns None if the variable is not set.
path_env = os.environ.get('PATH')
if path_env:
    print(f"PATH environment variable (first 100 chars): {path_env[:100]}...")
else:
    print("PATH environment variable not found.")

# Set a temporary environment variable (for the current process only)
os.environ['MY_APP_SETTING'] = 'production'
print(f"MY_APP_SETTING: {os.environ.get('MY_APP_SETTING')}")

# Delete an environment variable
if 'MY_APP_SETTING' in os.environ:
    del os.environ['MY_APP_SETTING']
    print(f"MY_APP_SETTING after deletion: {os.environ.get('MY_APP_SETTING')}")

 

 

Example 5: Running System Commands (Advanced)

 

# filename: std_lib_os_5.py

import os

# Execute a system command (output goes to stdout)
# os.system() returns the exit status of the command.
print("--- Running 'ls -l' or 'dir' command ---")
if os.name == 'posix': # Check if OS is Unix/Linux/macOS
    os.system("ls -l")
elif os.name == 'nt': # Check if OS is Windows
    os.system("dir")
else:
    print("Unsupported operating system for this command.")

# For capturing output, use subprocess module (more robust than os.system)
# import subprocess
# result = subprocess.run(['ls', '-l'], capture_output=True, text=True)
# print("\n--- Output from subprocess ---")
# print(result.stdout)

 

 

sys Module

The sys module provides access to system-specific parameters and functions. This module allows you to interact with the Python interpreter itself.

Example 1: Command Line Arguments (Beginner)

 

# filename: std_lib_sys_1.py

import sys

# sys.argv is a list containing the command-line arguments.
# sys.argv[0] is the script name itself.
print(f"Number of arguments: {len(sys.argv)}")
print(f"Argument list: {sys.argv}")

# How to run this: python std_lib_sys_1.py arg1 arg2 "another arg"
# Output example:
# Number of arguments: 4
# Argument list: ['std_lib_sys_1.py', 'arg1', 'arg2', 'another arg']

 

 

Example 2: Python Version Information (Beginner to Intermediate)

 

# filename: std_lib_sys_2.py

import sys

# Get Python version information
print(f"Python Version: {sys.version}")
print(f"Python Version Info: {sys.version_info}")
print(f"Major version: {sys.version_info.major}")
print(f"Minor version: {sys.version_info.minor}")

 

 

Example 3: Exiting the Program (Intermediate)

 

# filename: std_lib_sys_3.py

import sys

def check_value(value):
    if value < 0:
        print("Error: Value cannot be negative.")
        sys.exit(1) # Exit with an error code (non-zero indicates error)
    else:
        print(f"Value is {value}. Proceeding...")
        sys.exit(0) # Exit successfully (zero indicates success)

print("Checking value 10:")
check_value(10)
print("This line will not be reached if value is 10.") # This won't print due to sys.exit(0)

# You can try running this with a negative value to see the error exit.
# print("Checking value -5:")
# check_value(-5)
# print("This line will not be reached if value is -5.")

 

Example 4: Python Path (sys.path) (Intermediate to Advanced)

 

# filename: std_lib_sys_4.py

import sys

# sys.path is a list of strings that specifies the search path for modules.
# When you import a module, Python looks for it in these directories.
print("Python Module Search Path (sys.path):")
for path in sys.path:
    print(path)

# You can temporarily add a directory to sys.path
# sys.path.append("/path/to/my/custom/modules")

 

 

Example 5: Input/Output Streams (sys.stdin, sys.stdout, sys.stderr) (Advanced)

 

# filename: std_lib_sys_5.py

import sys

# Writing to standard output
sys.stdout.write("This is written to standard output.\n")

# Writing to standard error (useful for error messages that don't mix with regular output)
sys.stderr.write("This is an error message written to standard error.\n")

# Reading from standard input (similar to input() but lower-level)
# input_data = sys.stdin.readline().strip()
# print(f"You entered: {input_data}")

# Redirecting stdout temporarily (example, usually handled by context managers or logging)
original_stdout = sys.stdout
with open("output.log", "w") as f:
    sys.stdout = f # Redirect stdout to the file
    print("This line will go to output.log")
    print("Another line for the log file.")
sys.stdout = original_stdout # Restore stdout
print("This line goes to the console again.")

 

 

json Module (Introduction)

The json (JavaScript Object Notation) module allows you to work with JSON data. JSON is a lightweight data-interchange format that is easy for humans to read and write and easy for machines to parse2 and generate. It's widely used3 for web services and data storage.

Example 1: Encoding Python Dictionary to JSON String (Beginner)

 

# filename: std_lib_json_1.py

import json

# A Python dictionary
data = {
    "name": "Alice",
    "age": 30,
    "isStudent": False,
    "courses": ["Math", "Science", "History"]
}

# Convert Python dictionary to a JSON formatted string
json_string = json.dumps(data)
print(f"Python dictionary:\n{data}")
print(f"\nJSON string:\n{json_string}")

# Using indent for pretty-printing JSON
json_pretty_string = json.dumps(data, indent=4)
print(f"\nPretty-printed JSON string:\n{json_pretty_string}")

 

 

Example 2: Decoding JSON String to Python Dictionary (Beginner to Intermediate)

 

# filename: std_lib_json_2.py

import json

json_data_string = '{"product": "Laptop", "price": 1200.50, "in_stock": true}'

# Convert JSON string back to a Python dictionary
python_dict = json.loads(json_data_string)
print(f"JSON string:\n{json_data_string}")
print(f"\nPython dictionary:\n{python_dict}")
print(f"Product name: {python_dict['product']}")
print(f"Product price: {python_dict['price']}")

 

 

Example 3: Writing JSON to a File (Intermediate)

 

# filename: std_lib_json_3.py

import json

# Data to save
user_profile = {
    "username": "coder_guy",
    "email": "coder@example.com",
    "preferences": {
        "theme": "dark",
        "notifications": True
    }
}

file_name = "user_data.json"

# Write the Python dictionary to a JSON file
with open(file_name, "w") as json_file:
    json.dump(user_profile, json_file, indent=4) # Use indent for readability

print(f"User data saved to {file_name}")

# Verify by reading it back
with open(file_name, "r") as json_file:
    loaded_data = json.load(json_file)
    print(f"\nLoaded data from file:\n{loaded_data}")

 

 

Example 4: Reading JSON from a File (Intermediate to Advanced)

 

# filename: std_lib_json_4.py

import json
import os

# Create a dummy JSON file if it doesn't exist for this example
dummy_file_name = "settings.json"
if not os.path.exists(dummy_file_name):
    dummy_settings = {"api_endpoint": "https://api.example.com", "timeout": 30}
    with open(dummy_file_name, "w") as f:
        json.dump(dummy_settings, f, indent=4)
    print(f"Created dummy {dummy_file_name} for demonstration.")

# Read JSON data from a file
with open(dummy_file_name, "r") as json_file:
    settings = json.load(json_file)

print(f"Loaded settings:\n{settings}")
print(f"API Endpoint: {settings['api_endpoint']}")
print(f"Timeout: {settings['timeout']}")

# Clean up the dummy file
os.remove(dummy_file_name)
print(f"Removed dummy {dummy_file_name}.")

 

 

Example 5: Working with Lists of JSON Objects (Advanced)

 

# filename: std_lib_json_5.py

import json

# A list of dictionaries (Python objects)
employees = [
    {"id": 1, "name": "John Doe", "department": "HR"},
    {"id": 2, "name": "Jane Smith", "department": "Engineering"},
    {"id": 3, "name": "Peter Jones", "department": "Sales"}
]

# Convert the list of dictionaries to a JSON array string
employees_json_string = json.dumps(employees, indent=2)
print(f"Employees as JSON array:\n{employees_json_string}")

# Simulate receiving a JSON array string and parsing it
received_json_string = """
[
    {"city": "New York", "population": 8400000},
    {"city": "Los Angeles", "population": 3900000}
]
"""
cities_list = json.loads(received_json_string)
print(f"\nParsed cities list: {cities_list}")
print(f"First city: {cities_list[0]['city']}")

 

 

What are Packages?

While modules help organize code within a single file, packages take organization a step further. A Python package is a way of organizing related modules into a directory hierarchy. It's essentially a folder that contains multiple module files and a special __init__.py file.

Think of a package as a library or a collection of related toolboxes (modules). For instance, if you're building a web application, you might have separate packages for "authentication," "database," and "API handling."

Key benefits of packages:

  • Hierarchical Structure: Provides a clear, logical way to structure larger projects.
  • Avoids Name Collisions: By putting modules into different packages, you can have modules with the same name (e.g., utils.py in auth package and utils.py in database package) without conflict.
  • Easier Distribution: Packages are fundamental for distributing your code to others (e.g., via pip).

Organizing Modules into Packages

To turn a regular directory into a Python package, you simply need to place an (empty) file named __init__.py inside it.

Let's illustrate with an example project structure:

my_project/
├── main.py
└── my_package/
    ├── __init__.py
    ├── auth/
    │   ├── __init__.py
    │   └── user.py
    │   └── permissions.py
    └── data/
        ├── __init__.py
        └── database.py
        └── processing.py

Here, my_package is a package. Inside my_package, auth and data are sub-packages. user.py, permissions.py, database.py, and processing.py are modules within those sub-packages.

Example 1: Simple Package Structure (Beginner)

Let's create a package named shapes with a module for circle calculations.

project_root/
├── main.py
└── shapes/
    ├── __init__.py
    └── circle.py

 

shapes/circle.py:

 

# shapes/circle.py

# This module provides functions for circle calculations.
import math

def calculate_area(radius):
    """
    Calculates the area of a circle.
    Parameters:
        radius (float): The radius of the circle.
    Returns:
        float: The area of the circle.
    """
    return math.pi * radius**2

def calculate_circumference(radius):
    """
    Calculates the circumference of a circle.
    Parameters:
        radius (float): The radius of the circle.
    Returns:
        float: The circumference of the circle.
    """
    return 2 * math.pi * radius

 

 

shapes/__init__.py: (This file can be empty, but it makes shapes a package.)

 

# shapes/__init__.py
# This file makes 'shapes' a Python package.

 

 

main.py:

 

# main.py

# Import from the 'shapes' package.
from shapes import circle

radius = 5.0
area = circle.calculate_area(radius)
circumference = circle.calculate_circumference(radius)

print(f"Circle with radius {radius}:")
print(f"Area: {area:.2f}")
print(f"Circumference: {circumference:.2f}")

 

 

Example 2: Package with Multiple Modules (Beginner to Intermediate)

Let's expand the shapes package to include rectangle.

project_root/
├── main.py
└── shapes/
    ├── __init__.py
    ├── circle.py
    └── rectangle.py

 

 

shapes/rectangle.py:

 

# shapes/rectangle.py

# This module provides functions for rectangle calculations.

def calculate_area(length, width):
    """
    Calculates the area of a rectangle.
    Parameters:
        length (float): The length of the rectangle.
        width (float): The width of the rectangle.
    Returns:
        float: The area of the rectangle.
    """
    return length * width

def calculate_perimeter(length, width):
    """
    Calculates the perimeter of a rectangle.
    Parameters:
        length (float): The length of the rectangle.
        width (float): The width of the rectangle.
    Returns:
        float: The perimeter of the rectangle.
    """
    return 2 * (length + width)

 

 

main.py:

 

# main.py

from shapes import circle, rectangle # Import both modules

# Circle calculations
radius = 7.0
area_circle = circle.calculate_area(radius)
circ_circle = circle.calculate_circumference(radius)
print(f"Circle (radius {radius}): Area={area_circle:.2f}, Circumference={circ_circle:.2f}")

# Rectangle calculations
length = 10.0
width = 4.0
area_rect = rectangle.calculate_area(length, width)
perim_rect = rectangle.calculate_perimeter(length, width)
print(f"Rectangle (length {length}, width {width}): Area={area_rect:.2f}, Perimeter={perim_rect:.2f}")

 

 

Example 3: Nested Packages (Intermediate)

Let's create a more complex structure for a geometry package with shapes and measurements sub-packages.

project_root/
├── main.py
└── geometry/
    ├── __init__.py
    ├── shapes/
    │   ├── __init__.py
    │   ├── circle.py
    │   └── rectangle.py
    └── measurements/
        ├── __init__.py
        └── conversions.py

 

 

geometry/__init__.py: (empty)

geometry/shapes/__init__.py: (empty)

geometry/measurements/__init__.py: (empty)

geometry/measurements/conversions.py:

 

# geometry/measurements/conversions.py

# This module provides unit conversion functions.

def cm_to_inches(cm):
    """Converts centimeters to inches."""
    return cm * 0.393701

def inches_to_cm(inches):
    """Converts inches to centimeters."""
    return inches * 2.54

 

 

main.py:

 

# main.py

# Import modules from nested packages
from geometry.shapes import circle
from geometry.measurements import conversions

# Use functions from different modules/sub-packages
r = 6.0
print(f"Circle area (radius {r}): {circle.calculate_area(r):.2f}")

length_cm = 25.4
length_inches = conversions.cm_to_inches(length_cm)
print(f"{length_cm} cm is {length_inches:.2f} inches.")

length_back_to_cm = conversions.inches_to_cm(length_inches)
print(f"{length_inches:.2f} inches is {length_back_to_cm:.2f} cm.")

 

 

Example 4: Using __all__ in __init__.py for from package import * (Intermediate to Advanced)

The __init__.py file can be more than just an empty marker. It can execute initialization code for the package or define what gets imported when from package import * is used.

Modify shapes/__init__.py:

 

# shapes/__init__.py

# This defines what is imported when 'from shapes import *' is used.
__all__ = ["circle", "rectangle"] # Expose circle and rectangle modules directly

# You can also import specific functions to make them directly accessible from the package level
from .circle import calculate_area as circle_area
from .rectangle import calculate_perimeter as rect_perimeter

print("Shapes package initialized!")

 

 

main.py:

 

# main.py

# Example of 'from package import *' (use with caution in real projects)
# from shapes import * # This would import 'circle' and 'rectangle' modules directly.
# print(circle.calculate_area(3))

# Example of importing specific functions made available via __init__.py
from shapes import circle_area, rect_perimeter

print(f"Area using alias from package init: {circle_area(5):.2f}")
print(f"Rectangle perimeter using alias from package init: {rect_perimeter(5, 10):.2f}")

# You can still import modules normally
from shapes import rectangle
print(f"Rectangle area (imported module): {rectangle.calculate_area(5, 10):.2f}")

 

 

Example 5: Package with Resources or Data (Advanced)

Packages can also include non-Python files, like data files or configuration files. You'd typically access these using modules like importlib.resources (for Python 3.7+) or pkg_resources (from setuptools).

my_data_app/
├── main.py
└── data_package/
    ├── __init__.py
    ├── config.py
    └── data/
        └── users.csv
        └── default_settings.json

 

 

data_package/config.py:

 

# data_package/config.py

import os

# Define the base path for the data directory relative to the config.py file
# This assumes data/ is a sibling of config.py or in the same parent directory as config.py
# For more robust resource loading in installed packages, use importlib.resources
DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')

USERS_CSV_PATH = os.path.join(DATA_DIR, 'users.csv')
DEFAULT_SETTINGS_JSON_PATH = os.path.join(DATA_DIR, 'default_settings.json')

def get_users_data():
    """Reads data from users.csv and returns it as a list of dictionaries."""
    import csv
    users = []
    if os.path.exists(USERS_CSV_PATH):
        with open(USERS_CSV_PATH, 'r', newline='') as csvfile:
            reader = csv.DictReader(csvfile)
            for row in reader:
                users.append(row)
    return users

def get_default_settings():
    """Reads default settings from default_settings.json."""
    import json
    if os.path.exists(DEFAULT_SETTINGS_JSON_PATH):
        with open(DEFAULT_SETTINGS_JSON_PATH, 'r') as json_file:
            return json.load(json_file)
    return {}

 

 

data_package/__init__.py: (empty)

data_package/data/users.csv:

Code snippet

 

id,name,email
1,Alice,alice@example.com
2,Bob,bob@example.com

 

 

data_package/data/default_settings.json:

JSON

 

{
    "app_name": "MyDataApp",
    "version": "1.0",
    "logging_enabled": true
}

 

 

main.py:

 

# main.py

from data_package import config
import os

# Ensure the data directory and files exist for this example to run
# In a real deployed package, these files would be included in the package distribution.
# For simplicity, we'll create them here if they don't exist.

# Create data directory if it doesn't exist
if not os.path.exists(os.path.join(os.path.dirname(config.__file__), 'data')):
    os.makedirs(os.path.join(os.path.dirname(config.__file__), 'data'))

# Create users.csv if it doesn't exist
if not os.path.exists(config.USERS_CSV_PATH):
    with open(config.USERS_CSV_PATH, 'w', newline='') as f:
        f.write("id,name,email\n")
        f.write("1,Alice,alice@example.com\n")
        f.write("2,Bob,bob@example.com\n")

# Create default_settings.json if it doesn't exist
if not os.path.exists(config.DEFAULT_SETTINGS_JSON_PATH):
    import json
    with open(config.DEFAULT_SETTINGS_JSON_PATH, 'w') as f:
        json.dump({"app_name": "MyDataApp", "version": "1.0", "logging_enabled": True}, f, indent=4)


print(f"Users data: {config.get_users_data()}")
print(f"Default settings: {config.get_default_settings()}")

# Clean up created files/directories (optional)
# os.remove(config.USERS_CSV_PATH)
# os.remove(config.DEFAULT_SETTINGS_JSON_PATH)
# os.rmdir(os.path.join(os.path.dirname(config.__file__), 'data'))

 

 

__init__.py File

The __init__.py file is a crucial component of a Python package. Its presence informs Python that the directory it resides in should be treated as a package.

Key roles of __init__.py:

  • Package Identifier: Its mere existence signals that a directory is a Python package, even if it's empty.
  • Initialization Code: It can contain initialization code that is executed when the package (or one of its modules) is imported. This is useful for setting up package-wide configurations, registering components, or performing initial setup tasks.
  • Controlling from package import *: It can define the __all__ variable, which specifies which names are imported when a wildcard import (from package import *) is used.
  • Facilitating Package-level Imports: You can import sub-modules or functions directly into the package's namespace, making them accessible at the package level.

Example 1: The Simplest __init__.py (Beginner)

An empty __init__.py is sufficient to define a package.

my_simple_package/
├── __init__.py
└── greetings.py

 

 

my_simple_package/__init__.py:

 

# my_simple_package/__init__.py
# This empty file makes 'my_simple_package' a Python package.

 

 

my_simple_package/greetings.py:

 

# my_simple_package/greetings.py
def say_hello(name):
    return f"Hello, {name}!"

 

 

main.py:

 

# main.py
from my_simple_package import greetings
print(greetings.say_hello("World"))

 

 

Example 2: __init__.py with Initialization Message (Beginner to Intermediate)

my_app_package/
├── __init__.py
└── core.py

 

 

my_app_package/__init__.py:

 

# my_app_package/__init__.py
print("Initializing my_app_package...") # This will print once when the package is first imported.

VERSION = "1.0.0" # Define a package-level constant
AUTHOR = "Your Name"

# Import core functionality to be directly accessible from the package
from .core import process_data

 

 

my_app_package/core.py:

 

# my_app_package/core.py
def process_data(data):
    return [item.upper() for item in data]

 

 

main.py:

 

# main.py
import my_app_package # This triggers the __init__.py code execution

print(f"App version: {my_app_package.VERSION}")
print(f"App author: {my_app_package.AUTHOR}")

data_items = ["apple", "banana", "cherry"]
processed = my_app_package.process_data(data_items) # Access function directly from package
print(f"Processed data: {processed}")

 

 

Example 3: __init__.py with __all__ (Intermediate)

This is useful for controlling what from package import * imports.

my_tools/
├── __init__.py
├── string_tools.py
└── list_tools.py

 

 

my_tools/__init__.py:

 

# my_tools/__init__.py
__all__ = ["string_tools", "list_tools"] # Only these modules will be imported with '*'

# You can also bring specific functions to the top-level package namespace
from .string_tools import capitalize_first_letter

 

 

my_tools/string_tools.py:

 

# my_tools/string_tools.py
def capitalize_first_letter(text):
    return text.capitalize()

def reverse_text(text):
    return text[::-1]

 

 

my_tools/list_tools.py:

 

# my_tools/list_tools.py
def get_unique_items(items):
    return list(set(items))

def sort_list_in_place(items):
    items.sort()

 

 

main.py:

 

# main.py
from my_tools import * # Imports string_tools and list_tools due to __all__

print(string_tools.reverse_text("hello"))
my_list = [3, 1, 4, 1, 5]
sort_list_in_place(my_list) # This function is NOT in string_tools or list_tools, but was not exposed by `__all__`.
# Wait, this example has a slight error. The `__all__` applies to `from my_tools import *`.
# `sort_list_in_place` would be directly importable if explicitly put in __init__.py,
# or accessed via `list_tools.sort_list_in_place`.
# Let's fix this for clarity:

# Corrected main.py for __all__ example
# main.py
from my_tools import * # Imports string_tools and list_tools modules

# Now you can access functions via the module name:
print(string_tools.capitalize_first_letter("python"))
print(string_tools.reverse_text("world"))

numbers = [5, 2, 8, 1, 2, 5]
print(f"Unique numbers: {list_tools.get_unique_items(numbers)}")

# You can also use functions brought directly into the package's namespace
from my_tools import capitalize_first_letter # This comes from __init__.py
print(f"Capitalized via __init__.py: {capitalize_first_letter('tutorial')}")

# Note: if sort_list_in_place wasn't directly imported or put in __all__,
# you would still need list_tools.sort_list_in_place()

 

 

Example 4: Relative Imports within __init__.py (Intermediate to Advanced)

__init__.py often uses relative imports to make sub-modules accessible directly from the package.

complex_package/
├── __init__.py
├── validators.py
└── utils/
    ├── __init__.py
    └── helpers.py

 

 

complex_package/__init__.py:

 

# complex_package/__init__.py

# Bring some top-level functions directly into the package namespace
from .validators import is_valid_email
from .utils.helpers import format_name # Relative import from a sub-package

 

 

complex_package/validators.py:

 

# complex_package/validators.py
import re

def is_valid_email(email):
    return re.match(r"[^@]+@[^@]+\.[^@]+", email) is not None

 

 

complex_package/utils/__init__.py: (empty)

complex_package/utils/helpers.py:

 

# complex_package/utils/helpers.py
def format_name(first, last):
    return f"{first.capitalize()} {last.capitalize()}"

 

 

main.py:

 

# main.py
import complex_package

print(f"Is 'test@example.com' a valid email? {complex_package.is_valid_email('test@example.com')}")
print(f"Is 'invalid-email' a valid email? {complex_package.is_valid_email('invalid-email')}")

formatted = complex_package.format_name("john", "doe")
print(f"Formatted name: {formatted}")

 

 

Example 5: Dynamic Imports and Package Structure (Advanced)

For very large packages, __init__.py might dynamically import parts of the package or define complex package behavior.

my_api_client/
├── __init__.py
├── resources/
│   ├── __init__.py
│   ├── users.py
│   └── products.py
└── client.py

 

 

my_api_client/__init__.py:

 

# my_api_client/__init__.py

# Define the API client version
__version__ = "0.1.0"

# Import main client class
from .client import APIClient

# Conditionally import resources or make them accessible via the client
# This is a common pattern where sub-modules are attached to a main client object.
# For example, APIClient might have attributes for users and products resources.
# Or you can expose them directly for convenience.
from .resources import users
from .resources import products

# You might expose specific functions/classes directly if they are frequently used
from .resources.users import get_user_by_id

 

 

my_api_client/resources/__init__.py: (empty)

my_api_client/resources/users.py:

 

# my_api_client/resources/users.py
def get_user_by_id(user_id):
    print(f"Fetching user with ID: {user_id}")
    return {"id": user_id, "name": f"User {user_id}", "email": f"user{user_id}@example.com"}

def create_user(data):
    print(f"Creating user: {data['name']}")
    return {"status": "success", "user_id": 999}

 

 

my_api_client/resources/products.py:

 

# my_api_client/resources/products.py
def get_product_details(product_id):
    print(f"Fetching product with ID: {product_id}")
    return {"id": product_id, "name": f"Product {product_id}", "price": 10.99}

 

 

my_api_client/client.py:

 

# my_api_client/client.py
from .resources import users, products

class APIClient:
    def __init__(self, api_key):
        self.api_key = api_key
        print(f"APIClient initialized with key: {api_key[:5]}...")
        # Attach resource modules as attributes for easier access
        self.users = users
        self.products = products

    def authenticate(self):
        print("Authenticating API client...")
        return True

 

 

main.py:

 

# main.py
import my_api_client

print(f"API Client Version: {my_api_client.__version__}")

# Use the directly exposed function from __init__.py
user_1 = my_api_client.get_user_by_id(1)
print(f"Fetched user: {user_1}")

# Create an instance of the APIClient
client = my_api_client.APIClient("my_super_secret_key_123")
client.authenticate()

# Access resources via the client object
user_data = client.users.create_user({"name": "New User"})
print(f"New user status: {user_data}")

product_data = client.products.get_product_details(101)
print(f"Product details: {product_data}")

 

 

Relative and Absolute Imports

When importing modules within a package, you have two primary ways to specify the import path: absolute imports and relative imports.

  • Absolute Imports: Specify the full path from the project's root (or Python's sys.path). They are generally preferred as they are clear, unambiguous, and less prone to breaking when the package structure changes.
  • Relative Imports: Specify the path relative to the current module's location within the package. They use leading dots (., .., etc.) to indicate the relative position. Useful for imports within a single package, making the package more self-contained.

Let's assume the following package structure for our examples:

my_application/
├── main.py
└── core_app/
    ├── __init__.py
    ├── api/
    │   ├── __init__.py
    │   └── handlers.py
    │   └── serializers.py
    └── utils/
        ├── __init__.py
        └── validators.py
        └── decorators.py

 

 

Absolute Imports

An absolute import uses the full package name starting from a top-level package on sys.path.

Example 1: Basic Absolute Import (Beginner)

From main.py importing handlers from core_app.api.

 

# my_application/main.py

# Absolute import: Starts from the top-level package
from core_app.api import handlers

print("Running main.py using absolute import...")
handlers.handle_request("GET", "/users")

 

 

core_app/__init__.py: (empty)

core_app/api/__init__.py: (empty)

core_app/api/handlers.py:

 

# core_app/api/handlers.py
def handle_request(method, path):
    print(f"Handling {method} request for {path}")

 

 

Example 2: Absolute Import within a Module (Beginner to Intermediate)

From core_app/api/handlers.py importing validators from core_app.utils.

 

# core_app/api/handlers.py

# Absolute import of a module from another sub-package
from core_app.utils import validators

def handle_request(method, path):
    print(f"Handling {method} request for {path}")
    if path == "/users" and method == "POST":
        if validators.is_valid_id("abc"):
            print("Invalid ID detected during request handling.")
        else:
            print("ID is valid.")

 

 

core_app/utils/__init__.py: (empty)

core_app/utils/validators.py:

 

# core_app/utils/validators.py
def is_valid_id(data_id):
    # For demonstration, let's say IDs must be digits only
    return data_id.isdigit()

 

 

Example 3: Absolute Import of Specific Item (Intermediate)

From core_app/api/handlers.py importing serialize_data directly from core_app.api.serializers.

 

# core_app/api/handlers.py

from core_app.utils import validators
# Absolute import of a specific function
from core_app.api.serializers import serialize_data

def handle_request(method, path, data=None):
    print(f"Handling {method} request for {path}")
    if data:
        print(f"Received data: {serialize_data(data)}") # Using the imported function directly
    if path == "/users/123":
        if validators.is_valid_id("123"):
            print("User ID 123 is valid.")

 

 

core_app/api/serializers.py:

 

# core_app/api/serializers.py
def serialize_data(data):
    return f"Serialized: {data}"

 

 

Example 4: Using Aliases with Absolute Imports (Intermediate to Advanced)

Aliasing can make long absolute import paths more manageable.

 

# core_app/api/handlers.py

# Alias the validators module
from core_app.utils import validators as v
from core_app.api.serializers import serialize_data as s

def handle_request(method, path, data=None):
    print(f"Handling {method} request for {path}")
    if data:
        print(f"Received data: {s(data)}") # Using aliased function
    if path.startswith("/items/"):
        item_id = path.split('/')[-1]
        if v.is_valid_id(item_id): # Using aliased module
            print(f"Item ID '{item_id}' is valid.")
        else:
            print(f"Item ID '{item_id}' is invalid.")

 

 

Example 5: Best Practice with Absolute Imports (Advanced)

Generally, it's good practice to import at the top of the file and use absolute imports for clarity.

 

# core_app/utils/decorators.py
# No imports needed here for this example.
def log_execution(func):
    def wrapper(*args, **kwargs):
        print(f"--- Entering {func.__name__} ---")
        result = func(*args, **kwargs)
        print(f"--- Exiting {func.__name__} ---")
        return result
    return wrapper

# core_app/api/handlers.py (Modified to use absolute imports and decorators)
from core_app.utils import validators # Absolute import
from core_app.api.serializers import serialize_data # Absolute import
from core_app.utils.decorators import log_execution # Absolute import

@log_execution
def handle_user_creation(user_data):
    if validators.is_valid_id(user_data.get('id', '')):
        print(f"User data valid: {serialize_data(user_data)}")
        # Logic to create user
    else:
        print("Invalid user ID in data.")

# main.py
from core_app.api import handlers

handlers.handle_user_creation({"id": "123", "name": "Alice"})
handlers.handle_user_creation({"id": "abc", "name": "Bob"})

 

 

Relative Imports

Relative imports specify the resource to import relative to the current location. They use dots:

  • .: Current package.
  • ..: Parent package.
  • ...: Grandparent package, and so on.

Example 1: Intra-module Relative Import (Beginner)

From core_app/api/handlers.py importing serializers from the same api package.

 

# core_app/api/handlers.py

# Relative import: . means current package
from . import serializers # Import serializers.py from the current (api) package

def handle_request(method, path, data=None):
    print(f"Handling {method} request for {path}")
    if data:
        print(f"Received data: {serializers.serialize_data(data)}") # Use directly

 

 

core_app/api/serializers.py: (Same as before)

 

# core_app/api/serializers.py
def serialize_data(data):
    return f"Serialized: {data}"

 

 

main.py:

 

# my_application/main.py
from core_app.api import handlers
handlers.handle_request("POST", "/new_item", {"name": "Laptop", "price": 1200})

 

 

Example 2: Relative Import from a Sibling Module (Beginner to Intermediate)

From core_app/api/serializers.py importing something from core_app/api/handlers.py. (Less common, but demonstrates syntax).

 

# core_app/api/serializers.py

# Relative import: . means current package (api)
from .handlers import handle_request as api_handler # Import a function from a sibling module

def serialize_data(data):
    # Imagine some serialization logic here, then perhaps logging it via a handler
    serialized = f"Serialized: {data}"
    api_handler("LOG", f"/data_serialization_log?data={serialized}") # Call sibling's function
    return serialized

 

 

Example 3: Relative Import from a Parent Package (Intermediate)

From core_app/api/handlers.py importing validators from core_app.utils.

 

# core_app/api/handlers.py

# Relative import: .. means parent package (core_app), then from there go to utils
from ..utils import validators

def handle_request(method, path, data=None):
    print(f"Handling {method} request for {path}")
    if data and validators.is_valid_email("test@example.com"): # Assuming validators has is_valid_email
        print(f"Data valid: {data}")
    else:
        print("Data invalid or no data.")

 

 

core_app/utils/validators.py:

 

# core_app/utils/validators.py
import re
def is_valid_email(email):
    return re.match(r"[^@]+@[^@]+\.[^@]+", email) is not None

 

 

Example 4: Relative Import from a Grandparent Package (Intermediate to Advanced)

From core_app/api/serializers.py importing VERSION from core_app/__init__.py.

 

# core_app/api/serializers.py

# Relative import: .. means parent package (core_app/api), then another .. means grandparent (core_app)
from ... import VERSION # Accessing VERSION defined in core_app/__init__.py

def serialize_data(data):
    print(f"Using API version: {VERSION}")
    return f"Serialized: {data}"

 

 

core_app/__init__.py:

 

# core_app/__init__.py
VERSION = "2.0"

 

 

Example 5: When to Use Relative vs. Absolute (Advanced)

  • Absolute imports are generally preferred for clarity and robustness, especially for imports from different top-level packages or when a module might be moved.
  • Relative imports are good for keeping imports within a sub-package consistent and making it easier to move a sub-package around within a larger project, as long as its internal structure relative to other sub-modules remains the same.

 

# core_app/api/handlers.py

# Prefer absolute imports for clarity and consistency across the project
from core_app.utils import validators
from core_app.api import serializers # Though '.serializers' would also work, this is explicit.

# Relative imports are fine for sibling modules in the same sub-package if it makes sense
# from . import serializers # This is also valid and commonly used for siblings.

def process_api_request(request_data):
    if validators.is_valid_email(request_data.get('email', '')):
        print("Email is valid.")
        response = serializers.serialize_data(request_data)
        return response
    else:
        print("Invalid email provided.")
        return "Error: Invalid Email"

# main.py
from core_app.api.handlers import process_api_request
print(process_api_request({"email": "user@domain.com", "message": "hello"}))
print(process_api_request({"email": "bad-email", "message": "oops"}))

 

 

Package Management with pip

pip is the standard package manager for Python. It allows you to install, uninstall, and manage Python packages from the Python Package Index (PyPI)4 and other sources. Learning to use pip is fundamental for any Python developer.

Installing, Uninstalling, Listing Packages

Example 1: Installing a Basic Package (Beginner)

Let's install the requests library, a very popular HTTP client for Python.

Bash

 

# In your terminal or command prompt:
pip install requests

(No Python code here, this is a terminal command. After running this, you can now import requests in your Python scripts.)

 

To demonstrate its use in code:

 

# filename: pip_install_requests.py
import requests

try:
    response = requests.get("https://www.google.com")
    print(f"Successfully fetched Google.com! Status code: {response.status_code}")
except requests.exceptions.RequestException as e:
    print(f"Error fetching page: {e}")

 

 

Example 2: Installing a Specific Version (Beginner to Intermediate)

Sometimes you need a particular version of a package for compatibility or testing.

Bash

 

# In your terminal:
pip install "Django==3.2.0" # Installs exactly version 3.2.0
# Or
pip install "numpy>=1.20.0" # Installs 1.20.0 or higher
pip install "pandas<2.0.0" # Installs any version less than 2.0.0

(No Python code, just pip commands.)

 

 

Example 3: Listing Installed Packages (Beginner)

To see all packages currently installed in your Python environment.

Bash

 

# In your terminal:
pip list
# Or, for more detail (including versions and locations):
pip freeze

(No Python code, just pip commands.)

 

 

Example 4: Uninstalling a Package (Beginner)

If you no longer need a package, you can uninstall it.

Bash

 

# In your terminal:
pip uninstall requests
# pip will ask for confirmation (y/n). You can add -y for no prompt.
# pip uninstall -y requests

(No Python code, just pip commands.)

 

 

Example 5: Upgrading a Package (Intermediate)

To get the latest version of an already installed package.

Bash

 

# In your terminal:
pip install --upgrade requests
# Or a specific version
# pip install --upgrade "requests==2.28.0"

(No Python code, just pip commands.)

 

 

requirements.txt

The requirements.txt file is a plain text file that lists all the Python package dependencies for a project, along with their exact versions. This is crucial for:

  • Reproducibility: Ensures that anyone (or any deployment system) working on the project uses the exact same versions of libraries, preventing "it works on my machine" issues.
  • Collaboration: Makes it easy for new team members to set up their development environment.
  • Deployment: Cloud platforms and CI/CD pipelines often use requirements.txt to install dependencies.

 

Example 1: Generating requirements.txt (Beginner)

After you've installed all your project's dependencies using pip install, you can generate this file.

Bash

 

# In your project's root directory:
pip freeze > requirements.txt

(No Python code here. This command takes all currently installed packages and their versions and writes them to requirements.txt.)

Example requirements.txt content:

Django==4.2.1
Pillow==9.4.0
requests==2.31.0

 

 

Example 2: Installing from requirements.txt (Beginner)

When you get a new project or set up a new environment, you can install all dependencies listed in the file.

Bash

 

# In your project's root directory:
pip install -r requirements.txt

(No Python code, just pip command.)

 

 

Example 3: Manual Editing of requirements.txt (Intermediate)

You can manually edit the requirements.txt file to specify exact versions or version ranges, and to add comments.

# requirements.txt

# Core web framework
Django==4.2.1

# For image processing
Pillow>=9.0.0,<10.0.0

# HTTP client for making requests
requests==2.31.0

# Database driver (if using PostgreSQL)
psycopg2-binary~=2.9.0 # Compatible release, e.g., 2.9.x

 

 

Example 4: Managing Development vs. Production Dependencies (Advanced)

For larger projects, you might have separate requirements.txt files for different environments (e.g., requirements-dev.txt, requirements-prod.txt).

requirements.txt (production dependencies):

requests==2.31.0
Flask==2.3.2
SQLAlchemy==2.0.15

requirements-dev.txt (development dependencies, including production ones):

-r requirements.txt # Include all production dependencies
pytest==7.4.0
flake8==6.0.0
ipython==8.14.0

 

To install development dependencies:

Bash

 

pip install -r requirements-dev.txt

 

 

Example 5: Best Practices with Virtual Environments (Advanced - Crucial!)

While pip manages packages, it's highly recommended to use virtual environments (venv or conda) for each project. This isolates project dependencies, preventing conflicts between different projects and keeping your global Python installation clean.

  1. Create a virtual environment: Bash

     

    python -m venv venv_name # (e.g., python -m venv .venv)
    
  2. Activate the virtual environment:
    • On Windows: .\venv_name\Scripts\activate
    • On macOS/Linux: source venv_name/bin/activate (Your terminal prompt will often change to indicate the active virtual environment)
  3. Install dependencies within the virtual environment: Bash

     

    pip install -r requirements.txt
    
  4. Deactivate the virtual environment: Bash

     

    deactivate
    

This ensures that pip install and pip freeze only affect the packages within that specific project's isolated environment.

Congratulations! You've now gained a solid understanding of Python modules and packages, along with how to manage your project dependencies using pip. These are fundamental concepts that will empower you to build well-structured, maintainable, and shareable Python applications. Keep practicing, and happy coding!