# Part 1: Getting Started with Python 3.12 - Setup, pyproject.toml, and Language Features

## Introduction

I've been writing Python professionally for a while, but it wasn't until I built [ansible-inspec](https://github.com/Htunn/ansible-inspec) — an open-source compliance automation tool — that I really felt the upgrade from older Python habits to what Python 3.12 offers. The project ships a FastAPI server, a CLI, async job execution, Pydantic v2 models, and a Prisma ORM integration, all in a single `pyproject.toml`-based package. Everything in this series is grounded in that real codebase.

This first part covers getting Python 3.12 running the right way, understanding the project structure, and a tour of 3.12 language features I actually reach for.

***

## Why Python 3.12?

When I started `ansible-inspec`, I had a choice of runtime target. I settled on 3.12 (while keeping ≥ 3.8 compatibility in the metadata for now) because of:

* **`tomllib` in the standard library** — useful for reading config without extra deps
* **Better error messages** — `NameError`, `SyntaxError`, and `ImportError` now tell you *exactly* what's wrong and why
* **`typing` improvements** — `override`, `TypeVar` with defaults, `@dataclass_transform` — all reducing boilerplate in Pydantic models
* **Performance** — CPython 3.12 is \~5% faster than 3.11 in most benchmarks; not dramatic, but free
* **`f-string` nesting** — finally legal in 3.12, useful when building dynamic log lines

***

## Installing Python 3.12

### macOS

```bash
# The cleanest way on macOS is pyenv
brew install pyenv

# Add to your shell (zsh example)
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zshrc
echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zshrc
echo 'eval "$(pyenv init -)"' >> ~/.zshrc
source ~/.zshrc

# Install Python 3.12
pyenv install 3.12.3
pyenv global 3.12.3

# Verify
python --version   # Python 3.12.3
```

### Linux (Ubuntu/Debian)

```bash
sudo apt update
sudo apt install -y software-properties-common
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt update
sudo apt install -y python3.12 python3.12-venv python3.12-dev

python3.12 --version
```

### Windows

Download the official installer from [python.org/downloads](https://www.python.org/downloads/) and tick "Add Python to PATH".

***

## Project Setup

### Virtual Environment

Always isolate dependencies from the system Python:

```bash
# Create a venv using 3.12 explicitly
python3.12 -m venv .venv

# Activate
source .venv/bin/activate    # macOS/Linux
.venv\Scripts\activate       # Windows PowerShell

# Confirm
python --version   # Python 3.12.x
```

### Modern `pyproject.toml`

Python packaging has moved from `setup.py` to `pyproject.toml`. This is the structure I use in `ansible-inspec`:

```toml
[build-system]
requires = ["setuptools>=65.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "ansible-inspec"
version = "0.2.12"
description = "Compliance testing with Ansible and InSpec integration"
readme = "README.md"
authors = [{ name = "Htunn Thu Thu" }]
license = { text = "GPL-3.0-or-later" }
requires-python = ">=3.8"

classifiers = [
    "Programming Language :: Python :: 3.12",
    "Topic :: System :: Systems Administration",
    "Topic :: Security",
]

keywords = ["ansible", "inspec", "compliance", "automation"]

dependencies = [
    "fastapi>=0.115.0",
    "uvicorn[standard]>=0.30.0",
    "pydantic>=2.0.0",
    "pydantic-settings>=2.0.0",
    "httpx>=0.27.0",
    "pyyaml>=6.0",
    "jinja2>=3.1.0",
]

[project.optional-dependencies]
server = [
    "prisma>=0.15.0",
    "passlib[bcrypt]>=1.7.4",
    "msal>=1.26.0",
    "python-jose[cryptography]>=3.3.0",
    "streamlit>=1.40.0",
]
dev = [
    "pytest>=8.0.0",
    "pytest-asyncio>=0.23.0",
    "httpx>=0.27.0",
    "ruff>=0.4.0",
    "mypy>=1.10.0",
]

[project.scripts]
ansible-inspec = "ansible_inspec.cli.main:main"

[tool.setuptools.packages.find]
where = ["lib"]

[tool.ruff]
line-length = 100
target-version = "py312"

[tool.mypy]
python_version = "3.12"
strict = true

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
```

Key things to notice:

* `[project.optional-dependencies]` — install with `pip install -e ".[server,dev]"` for everything, or `pip install -e .` for CLI-only
* `[project.scripts]` — the `ansible-inspec` command maps directly to a Python function
* `[tool.ruff]` / `[tool.mypy]` / `[tool.pytest.ini_options]` — all tooling config lives in one file

Install the project in editable mode:

```bash
# CLI only
pip install -e .

# With server extras
pip install -e ".[server]"

# Everything (dev + server)
pip install -e ".[server,dev]"
```

***

## Python 3.12 Language Features I Actually Use

### Better Error Messages

```python
# In older Python you'd get: NameError: name 'config' is not defined
# In 3.12:
# NameError: name 'config' is not defined. Did you mean: 'Config'?

class Config:
    debug: bool = False

cfg = Config()
print(config.debug)  # 3.12 hints at the right name
```

The improved tracebacks in 3.12 point to the exact bracket or call that failed — a genuine daily-quality-of-life improvement.

### `match` Statement (Structural Pattern Matching, from 3.10+)

I use this in the CLI dispatcher for `ansible-inspec`:

```python
def dispatch_command(command: str, args: dict) -> None:
    match command:
        case "exec":
            run_profile(args["profile"], args["inventory"])
        case "convert":
            convert_profile(args["source"], args["output"])
        case "start-server":
            start_api_server(args.get("host", "0.0.0.0"), args.get("port", 8080))
        case _:
            print(f"Unknown command: {command}")
```

Much cleaner than a chain of `if/elif`.

### Improved `f-strings` (3.12)

In Python 3.11 and earlier, you could not reuse the same quote type inside an f-string expression. 3.12 removes that limitation:

```python
# Previously illegal — SyntaxError in <3.12
# Now valid in 3.12
hosts = ["web-01", "web-02", "db-01"]
msg = f"Hosts: {', '.join(h for h in hosts if h.startswith('web'))}"
print(msg)  # Hosts: web-01, web-02

# Also valid now — nested f-strings
label = "compliance"
log = f"Running {f'{label.upper()} check'} on {len(hosts)} hosts"
print(log)  # Running COMPLIANCE check on 3 hosts
```

### Type Alias (`type` keyword, 3.12)

```python
# Old style (still valid)
from typing import TypeAlias
HostList: TypeAlias = list[str]

# 3.12 — first-class `type` statement
type HostList = list[str]
type ProfileResult = dict[str, bool | str | None]

def get_results(hosts: HostList) -> list[ProfileResult]:
    return [{"host": h, "passed": True} for h in hosts]
```

### `TypeVar` with Default (3.13-preview, but `typing_extensions` backport works on 3.12)

Not in 3.12 stdlib yet, but worth knowing. For now, the `TypeVar` + `Generic` pattern is standard:

```python
from typing import TypeVar, Generic

T = TypeVar("T")

class JobResult(Generic[T]):
    def __init__(self, data: T, success: bool) -> None:
        self.data = data
        self.success = success

result: JobResult[dict] = JobResult(data={"host": "web-01"}, success=True)
```

***

## Project Structure

The structure I landed on for `ansible-inspec` — and which I recommend for any medium-sized Python project:

```
ansible-inspec/
├── lib/
│   └── ansible_inspec/         # src-layout keeps the package isolated
│       ├── __init__.py
│       ├── cli/
│       │   └── main.py
│       ├── core/
│       │   └── executor.py
│       ├── server/
│       │   ├── app.py
│       │   └── routes/
│       └── models/
│           └── job.py
├── tests/
│   ├── unit/
│   └── integration/
├── docs/
├── pyproject.toml
├── Makefile
└── README.md
```

The `lib/` src-layout (as opposed to putting the package at root) means you can't accidentally import the un-installed package during tests — `pip install -e .` is required, which is the correct behaviour.

The `Makefile` is a simple task runner:

```makefile
.PHONY: install dev-install test lint format

install:
	pip install -e .

dev-install:
	pip install -e ".[server,dev]"

test:
	pytest

lint:
	ruff check lib/ tests/
	mypy lib/

format:
	ruff format lib/ tests/
```

***

## `__init__.py` — The Package Entry Point

This is what `lib/ansible_inspec/__init__.py` looks like:

```python
"""
ansible-inspec: Compliance testing with Ansible and InSpec integration

Copyright (C) 2026 ansible-inspec project contributors
Licensed under GPL-3.0
"""

__version__ = "0.2.12"
__author__ = "Htunn Thu Thu"
__license__ = "GPL-3.0"

UPSTREAM_PROJECTS: dict[str, dict[str, str]] = {
    "ansible": {
        "url": "https://github.com/ansible/ansible",
        "license": "GPL-3.0",
        "copyright": "Red Hat, Inc.",
    },
    "inspec": {
        "url": "https://github.com/inspec/inspec",
        "license": "Apache-2.0",
        "copyright": "Progress Software Corp.",
    },
}
```

Notice the type annotation on `UPSTREAM_PROJECTS` — using `dict[str, dict[str, str]]` directly (lowercase), which is valid from Python 3.9+. No `from typing import Dict` needed.

***

## Summary

| Topic          | Takeaway                                                  |
| -------------- | --------------------------------------------------------- |
| Installation   | Use `pyenv` on macOS/Linux; official installer on Windows |
| Virtual env    | Always use one; `python3.12 -m venv .venv`                |
| Packaging      | `pyproject.toml` is the standard — no more `setup.py`     |
| `match`        | Cleaner than `if/elif` chains for command dispatch        |
| `f-strings`    | 3.12 removes quote nesting restrictions                   |
| `type` keyword | Cleaner type aliases in 3.12                              |
| Src-layout     | `lib/` directory prevents accidental bare imports         |

***

## What's Next

[Part 2](https://blog.htunnthuthu.com/getting-started/programming/python-101/python-101-part-2) covers the data structures, type hints, and Pydantic v2 models I use throughout the `ansible-inspec` server — including how job templates and results are modelled.
