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

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.