Part 2: Data Structures, Type Hints and Pydantic

Introduction

Building ansible-inspec meant modelling quite a few domain objects: job templates, execution results, profile metadata, credential bundles, and API request/response bodies. Python's native data structures get you far, but after a certain point you want the compiler (and runtime) to catch shape mismatches early. That's where Pydantic v2 earns its keep alongside Python 3.12's improved type annotation syntax.

This part covers the core data structures I reach for and how I model domain data for a real project.


Python's Built-in Data Structures

Lists

# Ordered, mutable, allows duplicates
hosts: list[str] = ["web-01", "web-02", "db-01"]

hosts.append("cache-01")          # add to end
hosts.insert(0, "lb-01")          # add at index
hosts.remove("db-01")             # remove by value
popped = hosts.pop()              # remove and return last item

# List comprehension โ€” I use this constantly in ansible-inspec
active_hosts = [h for h in hosts if h.startswith("web")]

# Slicing
first_two = hosts[:2]
reversed_hosts = hosts[::-1]

Dictionaries

The most-used structure in any Python automation code:

Sets

Useful for deduplication โ€” e.g. collecting unique failed controls across runs:

Tuples

Immutable sequences โ€” I use them for fixed-shape data like (host, port) pairs:


Type Hints in Python 3.12

Python is dynamically typed but type hints + mypy/pyright give you static checking. I run mypy --strict on ansible-inspec.

Basic annotations

Union types

Generics with built-in types (3.9+)

No more from typing import List, Dict, Tuple, Set โ€” use lowercase directly:

TypedDict โ€” typed dicts without a full class

Useful for dicts that come from JSON/YAML config files:

type alias (3.12)

@overload โ€” when a function returns different types based on input


Pydantic v2

Pydantic is the validation and serialisation library that powers FastAPI. ansible-inspec's API models are all Pydantic v2 models. The upgrade from v1 to v2 is significant โ€” v2 rewrote the core in Rust and adopted a stricter configuration model.

Basic model

Field validators

Nested models

The real power shows when you compose models:

JSON serialisation / deserialisation

model_config โ€” Pydantic v2 configuration

pydantic-settings โ€” environment-based config (used in ansible-inspec)

This pattern keeps configuration centralised and validated. Setting DATABASE__URL=... in your shell or .env file is automatically picked up.


Practical Patterns

Parsing YAML inventories

Serializing results to JSON


Summary

Concept
Key point

Lists

Ordered, mutable; use comprehensions liberally

Dicts

Ordered (3.7+); | merge operator (3.9+)

Sets

Deduplication, fast membership tests

TypedDict

Typed dict shapes without a full class

type alias

3.12 first-class alias keyword

Pydantic BaseModel

Validation + serialisation in one

Field()

Constraints, defaults, aliases

pydantic-settings

Env-var driven config with validation


What's Next

Part 3 digs into OOP, @dataclass, Protocol, and abstract base classes โ€” the patterns that make ansible-inspec's adapters and plugin architecture composable.

Last updated