Free — no signup required

Type Hinting and Static Analysis with Mypy

3 min read

The Problem with Dynamic Typing at Scale

Python's dynamic typing is a feature, not a bug — for small scripts. You can write x = 5 and then x = "hello" and Python won't complain. This flexibility speeds up prototyping.

But imagine a 10,000-line infrastructure codebase with 15 contributors. Someone changes a utility function to return a dict instead of a list. Every caller of that function now has a latent bug. Python won't tell you. Your tests might not catch it. You'll find out 20 minutes into a production deployment when AttributeError: 'dict' object has no attribute 'append' appears in your logs.

Type hints (introduced in Python 3.5, significantly expanded through 3.10+) let you annotate the expected types of function arguments and return values:

# Without type hints — what does this function expect? What does it return?
def provision(config, count):
    pass

# With type hints — unambiguous contract
from typing import Optional

def provision(config: dict[str, str], count: int) -> Optional[list[str]]:
    """Returns a list of provisioned hostnames, or None if provisioning failed."""
    pass

Python itself ignores these annotations at runtime — they are purely for humans and tools. The value comes from running a static analyzer.

Mypy: Catching Bugs Before Deployment

Mypy is the standard static type checker for Python. It reads your type hints and analyzes your code without executing it, reporting type mismatches as errors.

Consider this buggy infrastructure script:

# deploy.py
def get_replica_count(config: dict) -> int:
    return config["replicas"]

def scale_service(name: str, replicas: int) -> None:
    print(f"Scaling {name} to {replicas} replicas")

config = {"replicas": "3"}  # Bug: "3" is a string, not an int
count = get_replica_count(config)
scale_service("payment-api", count)  # Passing str where int is expected

Running this script produces no error — Python happily passes the string "3" to scale_service. But pointing Mypy at the same file surfaces it immediately:

deploy.py:9: error: Argument 2 to "scale_service" has incompatible type "str"; expected "int"
Found 1 error in 1 file (checked 1 source file)

Mypy caught the bug before the code ran. In a real deployment script, this might have caused a silent failure or a crash mid-deployment.

Key Type Hint Patterns for Ops Code

from typing import Optional, Union, Any
from collections.abc import Callable, Generator

# Optional: value can be the type OR None
def find_host(name: str) -> Optional[str]:
    return None  # or a hostname string

# Union: value can be one of several types (Python 3.10+: use X | Y syntax)
def parse_port(value: Union[str, int]) -> int:
    return int(value)

# Callable: a function as an argument
def run_with_retry(func: Callable[[], bool], attempts: int) -> bool:
    for _ in range(attempts):
        if func():
            return True
    return False

# dict and list with typed contents (Python 3.9+: use built-in dict/list directly)
def merge_configs(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
    return {**base, **override}

Integrating Mypy into CI

The real value of Mypy is running it automatically on every pull request, as a CI step that fails the build on any type error. A failing Mypy check blocks the merge, preventing type bugs from reaching production.

Key Point: Type hints are documentation that machines can verify. The combination of type hints + Mypy + CI enforcement creates a safety net that catches an entire class of bugs — wrong argument types, missing return values, None dereferences — before they ever reach a running system. This is especially valuable in Ops code where bugs can cause outages, not just test failures.

This is one of 18 chapters

Every chapter is free — Kubernetes, Terraform, SRE, distributed systems, and more — with fast daily review built in.

Sign up free