Python Typing

Type annotations, generics, TypeVar, and static type checking with mypy.

Basic Type Hints

Variable annotations — documentation for humans and tools, not enforced at runtime
hostname: str = "sw-core-01"
port: int = 443
enabled: bool = True
ratio: float = 0.95
Function signatures — parameter types and return type
def ping(host: str, count: int = 4) -> bool:
    ...

def get_config(device: str) -> str | None:
    ...

Optional and Union

Optional — value can be the type or None, two equivalent syntaxes
from typing import Optional

# These are equivalent (Python 3.10+)
def find_device(name: str) -> Optional[str]:    # older style
    ...

def find_device(name: str) -> str | None:       # modern style (preferred)
    ...
Union — value can be one of several types
# Python 3.10+ syntax
def parse_input(data: str | bytes | dict) -> dict:
    if isinstance(data, str):
        return json.loads(data)
    elif isinstance(data, bytes):
        return json.loads(data.decode())
    return data

Collection Types

Built-in generics — Python 3.9+ allows lowercase, no import needed
# Python 3.9+ (preferred)
hostnames: list[str] = ["sw1", "sw2"]
ip_map: dict[str, str] = {"sw1": "10.50.1.10"}
vlan_set: set[int] = {10, 20, 30}
coordinates: tuple[float, float] = (33.98, -118.45)

# Variable-length tuple
log_entries: tuple[str, ...] = ("entry1", "entry2", "entry3")
Nested generics — collections inside collections
# Dict mapping hostnames to list of interface names
interface_map: dict[str, list[str]] = {
    "sw1": ["Gi1/0/1", "Gi1/0/2"],
    "sw2": ["Gi1/0/1"],
}

# List of tuples (hostname, ip, port)
endpoints: list[tuple[str, str, int]] = [
    ("sw1", "10.50.1.10", 22),
    ("sw2", "10.50.1.11", 22),
]

TypedDict

TypedDict — typed dictionary with known keys, useful for API responses and structured data
from typing import TypedDict

class DeviceInfo(TypedDict):
    hostname: str
    ip: str
    model: str
    port: int

def get_device(name: str) -> DeviceInfo:
    return {"hostname": name, "ip": "10.50.1.10", "model": "C9300", "port": 22}

device = get_device("sw1")
device["hostname"]    # type checker knows this is str
TypedDict with optional keys — total=False makes all keys optional, or use NotRequired per-key
from typing import TypedDict, NotRequired

class DeviceConfig(TypedDict):
    hostname: str                     # required
    ip: str                           # required
    description: NotRequired[str]     # optional (Python 3.11+)
    tags: NotRequired[list[str]]      # optional

Protocol (Structural Typing)

Protocol — define an interface by structure, not inheritance. Any matching class satisfies it
from typing import Protocol

class Pingable(Protocol):
    hostname: str
    def ping(self) -> bool: ...

# This class satisfies Pingable without inheriting from it
class Switch:
    def __init__(self, hostname: str):
        self.hostname = hostname

    def ping(self) -> bool:
        return os.system(f"ping -c 1 {self.hostname}") == 0

def check_device(device: Pingable) -> str:
    return f"{device.hostname}: {'UP' if device.ping() else 'DOWN'}"

Literal

Literal — restrict values to specific constants, catches typos at type-check time
from typing import Literal

def set_port_mode(mode: Literal["access", "trunk", "dynamic"]) -> None:
    ...

set_port_mode("access")     # OK
set_port_mode("acces")      # type checker catches the typo
Literal with return types — narrow the return type to specific values
def get_status(host: str) -> Literal["up", "down", "unreachable"]:
    ...

Dataclasses

@dataclass — typed class with auto-generated init, repr, eq
from dataclasses import dataclass

@dataclass
class Endpoint:
    hostname: str
    ip: str
    port: int = 443
    tags: list[str] | None = None

# Type checker validates construction
e = Endpoint(hostname="sw1", ip="10.50.1.10")
e = Endpoint(hostname="sw1", ip=443)    # type error: ip should be str
Frozen dataclass — immutable, hashable, use for config and constants
@dataclass(frozen=True)
class VLANConfig:
    id: int
    name: str
    subnet: str

# Immutable -- can use as dict key or in sets
vlan10 = VLANConfig(10, "data", "10.50.10.0/24")
Dataclass with field() — mutable defaults, metadata, validators
from dataclasses import dataclass, field

@dataclass
class Switch:
    hostname: str
    interfaces: list[str] = field(default_factory=list)
    _session: object = field(default=None, repr=False, compare=False)

    def __post_init__(self):
        self.hostname = self.hostname.lower()

Callable

Callable type — specify function signatures as types
from typing import Callable

# Function that takes a string and returns bool
Validator = Callable[[str], bool]

def apply_validators(value: str, validators: list[Validator]) -> bool:
    return all(v(value) for v in validators)

# Usage
is_not_empty: Validator = lambda s: len(s) > 0
is_fqdn: Validator = lambda s: "." in s

apply_validators("sw1.domain.com", [is_not_empty, is_fqdn])

Type Aliases

Type alias — give complex types a readable name
# Simple alias
IPAddress = str
HostName = str

# Complex alias
DeviceMap = dict[str, list[tuple[IPAddress, int]]]

def get_topology() -> DeviceMap:
    return {
        "sw1": [("10.50.1.10", 22), ("10.50.1.10", 443)],
    }
TypeAlias (Python 3.10+) — explicit annotation for clarity
from typing import TypeAlias

InterfaceTable: TypeAlias = dict[str, list[str]]

Runtime Type Checking

isinstance — validate types at runtime, use for input validation
def process(data: str | dict) -> dict:
    if isinstance(data, str):
        return json.loads(data)
    if isinstance(data, dict):
        return data
    raise TypeError(f"Expected str or dict, got {type(data).__name__}")
Type guard (Python 3.10+) — tell the type checker about narrowing
from typing import TypeGuard

def is_string_list(val: list) -> TypeGuard[list[str]]:
    return all(isinstance(x, str) for x in val)

def process(items: list) -> None:
    if is_string_list(items):
        # type checker now knows items is list[str]
        print(", ".join(items))

Practical Typing: Network Automation

Typed device inventory — combining TypedDict, Literal, and generics
from typing import TypedDict, Literal

class InterfaceInfo(TypedDict):
    name: str
    status: Literal["up", "down", "admin-down"]
    vlan: int | None
    speed: str

class DeviceInventory(TypedDict):
    hostname: str
    model: str
    interfaces: list[InterfaceInfo]

def parse_show_interfaces(output: str) -> list[InterfaceInfo]:
    ...

def build_inventory(devices: list[str]) -> list[DeviceInventory]:
    ...