Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →
14 minLesson 17 of 34
Advanced Python

Modules, Packages & pip

Modules, Packages & pip: Organizing Code and Using the Python Ecosystem

Python's module system lets you split code into reusable files and use thousands of third-party libraries. Understanding it properly prevents the import errors and dependency conflicts that trip up beginners.

Modules: Single Files

Any .py file is a module. You import it to use its contents.

# math_utils.py
PI = 3.14159265358979

def circle_area(radius):
    return PI * radius ** 2

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

class Circle:
    def __init__(self, radius):
        self.radius = radius
    
    @property
    def area(self):
        return circle_area(self.radius)

# main.py — in the same directory
import math_utils

print(math_utils.PI)
print(math_utils.circle_area(5))

# Selective import
from math_utils import circle_area, PI

# Alias import
import math_utils as mu
print(mu.circle_area(5))

# Import everything (usually avoid this — pollutes namespace)
from math_utils import *

The if __name__ == '__main__' Pattern

Every module has a __name__ attribute. When run directly, __name__ == '__main__'. When imported, __name__ is the module name.

# calculator.py
def add(a, b):
    return a + b

def multiply(a, b):
    return a * b

def main():
    print("Running calculator demo:")
    print(f"3 + 4 = {add(3, 4)}")
    print(f"3 × 4 = {multiply(3, 4)}")

if __name__ == '__main__':
    # Only runs when this file is executed directly
    # NOT when imported as a module
    main()

# When you run: python calculator.py → main() executes
# When you run: import calculator → main() does NOT execute

This pattern is essential — without it, importing your module would run all its code.

Packages: Directories of Modules

A package is a directory containing an __init__.py file (can be empty).

myapp/
├── __init__.py           ← Makes myapp a package
├── config.py
├── models/
│   ├── __init__.py
│   ├── user.py
│   └── product.py
├── services/
│   ├── __init__.py
│   ├── auth.py
│   └── payment.py
└── utils/
    ├── __init__.py
    └── helpers.py
# Importing from packages
from myapp.models.user import User
from myapp.services.auth import authenticate
import myapp.config as config

# __init__.py can re-export for cleaner imports
# myapp/models/__init__.py
from .user import User
from .product import Product

# Now users can do:
from myapp.models import User, Product
# Instead of:
from myapp.models.user import User

Relative vs Absolute Imports

# myapp/services/auth.py

# Absolute import (always works, explicit)
from myapp.models.user import User
from myapp.config import SECRET_KEY

# Relative import (relative to current package)
from ..models.user import User    # Go up one level, then into models
from ..config import SECRET_KEY   # Go up one level, get config
from . import helpers             # Import from same package

Within a package, relative imports are common. In scripts, always use absolute imports.

pip: Installing Third-Party Packages

# Install a package
pip install requests

# Install specific version
pip install requests==2.31.0

# Install minimum version
pip install "requests>=2.25.0"

# Upgrade a package
pip install --upgrade requests

# Uninstall
pip uninstall requests

# List installed packages
pip list

# Show package info and dependencies
pip show requests

# Save current environment's packages
pip freeze > requirements.txt

# Install from requirements file
pip install -r requirements.txt

requirements.txt: Sharing Dependencies

# requirements.txt
requests==2.31.0
pandas>=2.0.0
numpy>=1.24.0
scikit-learn>=1.3.0
python-dotenv==1.0.0
fastapi>=0.100.0
uvicorn[standard]>=0.23.0

Always pin exact versions for production deployments. Use >= ranges for libraries you develop.

The Standard Library: Batteries Included

Python ships with an enormous standard library. Know what's available before installing third-party packages:

# pathlib — file paths (covered in File I/O lesson)
# json — JSON parsing
# csv — CSV reading/writing
# datetime — date and time handling
# re — regular expressions
# math — mathematical functions
# random — random number generation
# collections — specialized containers
# itertools — functional tools for iterables
# functools — higher-order functions
# os, sys — OS interface and system info
# subprocess — run external commands
# logging — logging (covered in error handling)
# typing — type hints
# dataclasses — data classes
# abc — abstract base classes
# threading, asyncio — concurrency
# unittest — testing framework
# argparse — command-line arguments
# hashlib — hashing (SHA, MD5)
# base64 — encoding
# time, timeit — timing and performance

# Examples
from datetime import datetime, timedelta
import random
from collections import Counter, defaultdict, deque
import itertools

# Counter — count occurrences
words = "the quick brown fox jumps over the lazy dog".split()
word_count = Counter(words)
print(word_count.most_common(3))  # [('the', 2), ('quick', 1), ('brown', 1)]

# deque — efficient queue
queue = deque(maxlen=5)  # Fixed-size queue
for i in range(10):
    queue.append(i)
print(queue)  # deque([5, 6, 7, 8, 9], maxlen=5)

# itertools
# Infinite counter
counter = itertools.count(10, 2)  # 10, 12, 14, ...
# Combinations
for combo in itertools.combinations("ABC", 2):
    print(combo)  # ('A', 'B'), ('A', 'C'), ('B', 'C')

# Chain iterables
from itertools import chain
numbers = list(chain([1, 2], [3, 4], [5, 6]))
print(numbers)  # [1, 2, 3, 4, 5, 6]

Essential Third-Party Libraries

# requests — HTTP requests
import requests
response = requests.get('https://api.github.com/users/python')
user = response.json()

# pandas — data analysis
import pandas as pd
df = pd.read_csv('data.csv')
print(df.head())

# numpy — numerical computing
import numpy as np
arr = np.array([1, 2, 3, 4, 5])
print(arr.mean(), arr.std())

# python-dotenv — load .env files
from dotenv import load_dotenv
import os
load_dotenv()
api_key = os.getenv('API_KEY')

# rich — beautiful terminal output
from rich import print
from rich.table import Table
print("[bold green]Hello, World![/bold green]")

# pydantic — data validation
from pydantic import BaseModel, validator
class User(BaseModel):
    name: str
    age: int
    email: str

Namespace Packages (Python 3.3+)

A directory without __init__.py is a namespace package — useful for large codebases split across multiple directories.

org/
├── team_a/
│   └── module_a.py    # No __init__.py needed!
└── team_b/
    └── module_b.py

# From separate locations, both are importable
from org.team_a.module_a import something
from org.team_b.module_b import other_thing

Finding Where a Module Comes From

import requests
import numpy as np

# Where is this module located?
print(requests.__file__)  # Path to requests package directory
print(np.__file__)        # Path to numpy

# What version?
print(requests.__version__)
print(np.__version__)

# What's in a module?
import math
print(dir(math))          # All attributes

# What does a function do?
help(math.log)
print(math.log.__doc__)   # Docstring

Next lesson: Virtual Environments & Project Setup — keeping dependencies isolated.

📱

Get this course's notes on Telegram!

Free cheat sheets, summaries & practice exercises

Get Notes Free →
!