Modules & Packages
Organize code into reusable modules. Build professional packages with modern tooling.
Modules
What is a Module?
A module is any Python file. When you import it, Python executes the file and makes its contents available.
# utils.py
"""Utility functions for network operations."""
DEFAULT_TIMEOUT = 30
def validate_ip(ip: str) -> bool:
"""Validate IPv4 address."""
parts = ip.split(".")
if len(parts) != 4:
return False
return all(p.isdigit() and 0 <= int(p) <= 255 for p in parts)
def validate_mac(mac: str) -> bool:
"""Validate MAC address."""
import re
return bool(re.match(r"^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$", mac))
class NetworkError(Exception):
"""Base exception for network errors."""
pass
Importing
# Import entire module
import utils
utils.validate_ip("10.50.1.20")
print(utils.DEFAULT_TIMEOUT)
# Import specific items
from utils import validate_ip, validate_mac
validate_ip("10.50.1.20")
# Import with alias
import utils as net_utils
net_utils.validate_ip("10.50.1.20")
# Import all (avoid - pollutes namespace)
from utils import *
validate_ip("10.50.1.20") # Works but unclear where it comes from
# Conditional import
try:
import httpx as http_client
except ImportError:
import requests as http_client
Module Search Path
import sys
# Python searches these directories in order
for path in sys.path:
print(path)
# 1. Current directory
# 2. PYTHONPATH environment variable
# 3. Standard library
# 4. Site-packages (installed packages)
# Add custom path at runtime
sys.path.insert(0, "/path/to/my/modules")
Module Execution
# utils.py
def main():
print("Testing utils...")
assert validate_ip("10.50.1.20") == True
assert validate_ip("invalid") == False
print("All tests passed!")
# Only runs when executed directly, not when imported
if __name__ == "__main__":
main()
# Run directly
python utils.py # Runs main()
# Import
python -c "import utils" # Doesn't run main()
Packages
Package Structure
A package is a directory with init.py.
netapi/
├── __init__.py # Makes it a package
├── cli/
│ ├── __init__.py
│ ├── ise.py
│ ├── vault.py
│ └── wlc.py
├── vendors/
│ ├── __init__.py
│ ├── cisco/
│ │ ├── __init__.py
│ │ ├── ise.py
│ │ └── wlc.py
│ └── hashicorp/
│ ├── __init__.py
│ └── vault.py
└── utils/
├── __init__.py
├── network.py
└── validation.py
init.py
# netapi/__init__.py
"""netapi - Infrastructure automation CLI."""
__version__ = "0.1.0"
__author__ = "Evan Rosado"
# Import commonly used items for convenience
from netapi.vendors.cisco.ise import ISEClient
from netapi.vendors.hashicorp.vault import VaultClient
# Define what "from netapi import *" exports
__all__ = ["ISEClient", "VaultClient", "__version__"]
# Usage
import netapi
print(netapi.__version__) # 0.1.0
from netapi import ISEClient # Works due to __init__.py imports
Relative Imports
# netapi/cli/ise.py
# Relative imports (within same package)
from . import common # Same directory
from .. import utils # Parent directory
from ..vendors.cisco import ise # Sibling package
# Absolute imports (always work)
from netapi.vendors.cisco.ise import ISEClient
from netapi.utils.validation import validate_mac
Package Import Patterns
# Import package
import netapi.vendors.cisco.ise
client = netapi.vendors.cisco.ise.ISEClient()
# Import module from package
from netapi.vendors.cisco import ise
client = ise.ISEClient()
# Import class from module
from netapi.vendors.cisco.ise import ISEClient
client = ISEClient()
# Import with alias
from netapi.vendors.cisco.ise import ISEClient as CiscoISE
client = CiscoISE()
pyproject.toml
Modern Python projects use pyproject.toml for configuration.
Minimal pyproject.toml
[project]
name = "netapi"
version = "0.1.0"
description = "Infrastructure automation CLI"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"click>=8.0",
"httpx>=0.24",
"rich>=13.0",
]
[project.scripts]
netapi = "netapi.cli.main:cli"
Full pyproject.toml
[project]
name = "netapi"
version = "0.1.0"
description = "Infrastructure automation CLI"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.11"
authors = [
{name = "Evan Rosado", email = "evan@example.com"}
]
keywords = ["cisco", "ise", "automation", "cli"]
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Console",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"click>=8.0",
"httpx>=0.24",
"rich>=13.0",
"pyyaml>=6.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"pytest-cov>=4.0",
"ruff>=0.1",
"mypy>=1.0",
]
docs = [
"mkdocs>=1.5",
"mkdocs-material>=9.0",
]
[project.scripts]
netapi = "netapi.cli.main:cli"
[project.urls]
Homepage = "https://github.com/user/netapi"
Documentation = "https://netapi.readthedocs.io"
Repository = "https://github.com/user/netapi"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.ruff]
line-length = 100
target-version = "py311"
select = ["E", "F", "I", "N", "W"]
[tool.mypy]
python_version = "3.11"
strict = true
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v --cov=netapi"
uv Package Manager
uv is the modern, fast Python package manager. Use it instead of pip/venv.
Project Setup
# Create new project
uv init netapi
cd netapi
# Project structure created:
# netapi/
# ├── pyproject.toml
# ├── README.md
# └── src/
# └── netapi/
# └── __init__.py
# Add dependencies
uv add click httpx rich
# Add dev dependencies
uv add --dev pytest pytest-cov ruff mypy
# Add optional dependency group
uv add --optional docs mkdocs mkdocs-material
Running Code
# Run Python
uv run python
# Run script
uv run python src/netapi/main.py
# Run module
uv run python -m netapi
# Run installed script
uv run netapi --help
# Run pytest
uv run pytest
# Run with specific Python version
uv run --python 3.12 python script.py
Dependency Management
# Install all dependencies
uv sync
# Install with dev dependencies
uv sync --dev
# Install with optional groups
uv sync --extra docs
# Upgrade dependencies
uv lock --upgrade
# Upgrade specific package
uv add click --upgrade
# Remove dependency
uv remove httpx
# Show dependency tree
uv tree
Lock File
uv creates uv.lock for reproducible installs:
# Install exact versions from lock file
uv sync
# Regenerate lock file
uv lock
# Check lock file is up to date
uv lock --check
Building & Publishing
# Build package
uv build
# Creates:
# dist/
# ├── netapi-0.1.0-py3-none-any.whl
# └── netapi-0.1.0.tar.gz
# Publish to PyPI
uv publish
# Publish to private index
uv publish --index-url https://private.pypi.org/simple/
Project Layouts
src Layout (Recommended)
netapi/
├── pyproject.toml
├── README.md
├── src/
│ └── netapi/
│ ├── __init__.py
│ ├── cli/
│ │ ├── __init__.py
│ │ └── main.py
│ └── vendors/
│ └── ...
└── tests/
├── __init__.py
├── test_cli.py
└── test_vendors/
Benefits: - Forces you to install package to test - No accidental imports from working directory - Clean separation of source and tests
Flat Layout
netapi/
├── pyproject.toml
├── README.md
├── netapi/
│ ├── __init__.py
│ └── ...
└── tests/
└── ...
Simpler but can cause import issues.
Practical Example: CLI Package
Structure
netapi/ ├── pyproject.toml ├── src/ │ └── netapi/ │ ├── __init__.py │ ├── cli/ │ │ ├── __init__.py │ │ ├── main.py # Entry point │ │ ├── ise.py # ISE commands │ │ └── vault.py # Vault commands │ ├── vendors/ │ │ ├── __init__.py │ │ └── cisco/ │ │ ├── __init__.py │ │ └── ise.py # ISE client │ └── utils/ │ ├── __init__.py │ └── output.py # Rich output helpers └── tests/
Entry Point
# src/netapi/cli/main.py
import click
from netapi.cli import ise, vault
@click.group()
@click.version_option()
def cli():
"""netapi - Infrastructure automation CLI."""
pass
# Register command groups
cli.add_command(ise.ise)
cli.add_command(vault.vault)
if __name__ == "__main__":
cli()
Command Group
# src/netapi/cli/ise.py
import click
from netapi.vendors.cisco.ise import ISEClient
from netapi.utils.output import console
@click.group()
@click.pass_context
def ise(ctx):
"""ISE operations."""
ctx.ensure_object(dict)
@ise.command()
@click.option("--host", required=True, help="ISE hostname")
@click.pass_context
def endpoints(ctx, host: str):
"""List all endpoints."""
client = ISEClient(host)
data = client.get_endpoints()
console.print(data)
pyproject.toml Entry
[project.scripts]
netapi = "netapi.cli.main:cli"
Usage
# Install in development mode
uv sync
# Run
uv run netapi --help
uv run netapi ise endpoints --host ise-01
Import Best Practices
# 1. Standard library first
import os
import sys
from pathlib import Path
# 2. Third-party packages
import click
import httpx
from rich.console import Console
# 3. Local imports
from netapi.vendors.cisco.ise import ISEClient
from netapi.utils.output import format_table
# 4. Avoid circular imports
# BAD: module_a imports module_b, module_b imports module_a
# FIX: Move shared code to module_c, or import inside functions
# 5. Use TYPE_CHECKING for type hints only
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from netapi.vendors.cisco.ise import ISEClient
def process(client: "ISEClient") -> None:
pass # ISEClient only imported for type checking, not runtime
# 6. Lazy imports for slow modules
def get_pandas_df():
import pandas as pd # Only imported when function called
return pd.DataFrame()
Next Module
File I/O - Reading, writing, paths, JSON/YAML.