11 · Python Development with AI
Python is the native language of the AI ecosystem. Nearly every model SDK, framework and research codebase ships Python first. Models are also very fluent in it. This page shows how to get high-quality Python from AI and avoid its typical traps.
By the end of this page you can
- Set up a modern Python toolchain that gives AI agents fast feedback.
- Prompt for typed, tested, idiomatic Python.
- Spot the most common AI mistakes in Python.
- Make your first LLM API call with only the standard library.
Why Python matters for AI work, even for Scala developers
- Most AI tooling (MCP servers, evals, notebooks, data science, fine-tuning) is easiest in Python.
- Prototype AI features in Python and productionise them in Scala/JVM if your platform requires it.
- Models generate excellent Python, which makes it a fast way to automate chores: scripts, data checks, log analysis.
A modern toolchain (fast feedback for agents)
| Tool | Purpose | Why it helps with AI |
|---|---|---|
| uv | Very fast package/project manager, virtual envs, Python versions | Reproducible envs. Agents can uv run anything without activation issues. |
| ruff | Linter + formatter (very fast) | Catches AI bugs (unused vars, bare except) in milliseconds. A great post-edit hook. |
| mypy / pyright | Static type checking | Gives Python some of the "compiler as reviewer" benefit Scala has. |
| pytest + hypothesis | Tests + property-based tests | Verification that agents can run themselves. |
| Jupyter / VS Code notebooks | Interactive exploration | AI assistants work inside notebooks for data exploration. |
| VS Code Python + Pylance, or PyCharm | IDE support | Type info and diagnostics feed the AI's context. |
uv init pricing-lib && cd pricing-lib uv add --dev pytest hypothesis ruff mypy uv run pytest -q uv run ruff check . --fix && uv run ruff format . uv run mypy src
Put these exact commands in AGENTS.md (see the Python example on page 07).
Prompting for good Python
- State the version: "Python 3.12" lets the model use modern syntax (
match,X | None, built-in generics likelist[int]). - Ask for types and docstrings: "Fully type-annotated, mypy --strict clean, Google-style docstrings."
- Name the libraries and versions: "pydantic v2", "pandas 2.x", "httpx not requests", "SQLAlchemy 2.0 style".
- Request structure: "Pure functions in core, I/O at the edges, dataclasses for data."
- Ask for tests in the same answer: "Then pytest tests with parametrize for edge cases."
Python 3.12, typed (mypy --strict), no external deps. Write `parse_duration(s: str) -> datetime.timedelta` accepting "90s", "15m", "2h", "1h30m", "1d2h". Raise ValueError with a helpful message on bad input. Then pytest tests using parametrize, including invalid inputs, and one hypothesis property (round-trip with a formatter you also write).
Common AI mistakes in Python (watch list)
| Mistake | Why it's bad | Ask for instead |
|---|---|---|
Mutable default args def f(x=[]) | Shared state between calls | x: list | None = None |
Bare except: or except Exception: pass | Swallows real errors | Catch specific exceptions; log or re-raise |
float for money | Rounding errors | decimal.Decimal |
| Outdated library APIs | pydantic v1 vs v2, old SDK call styles, pandas deprecations | State versions; paste docs; run tests |
| SQL via f-strings | SQL injection | Parameterised queries |
eval, pickle on untrusted data, shell=True | Code execution vulnerabilities | Safe parsers; subprocess.run([...]) |
| HTTP calls without timeouts | Hangs forever | httpx.get(url, timeout=10) |
Blocking calls inside async def | Freezes the event loop | Async clients or asyncio.to_thread |
| pandas row loops / chained assignment | Slow; SettingWithCopy bugs | Vectorised ops; .loc |
| Hallucinated packages | A package name that doesn't exist, or worse, a malicious look-alike ("slopsquatting") | Verify on PyPI before installing |
pip install / uv add of an
unfamiliar package, check that it exists, is maintained, and is the one you meant. The same applies to Maven/sbt coordinates.Tests as the safety net
import re
import pytest
from hypothesis import given, strategies as st
def slugify(title: str) -> str:
"""Lowercase, keep a-z0-9, collapse everything else into single hyphens."""
return re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-")
@pytest.mark.parametrize("raw, expected", [
("Hello World", "hello-world"),
(" Scala 3 & Python! ", "scala-3-python"),
("---", ""),
("", ""),
])
def test_examples(raw: str, expected: str) -> None:
assert slugify(raw) == expected
@given(st.text())
def test_properties(raw: str) -> None:
s = slugify(raw)
assert s == slugify(s) # idempotent
assert not s.startswith("-") and not s.endswith("-")
assert "--" not in s
assert re.fullmatch(r"[a-z0-9-]*", s)
uv run pytest -q test_slugify.py
Hypothesis generates hundreds of random inputs, including Unicode, and checks the properties. This kind of test catches the edge cases AI-written code most often misses.
Your first LLM call from Python
Standard library only, against a local OpenAI-compatible server (Ollama). For hosted providers, use their official SDK and an API key from an environment variable.
import json
import os
import sys
import urllib.request
ENDPOINT = os.environ.get("LLM_ENDPOINT", "http://localhost:11434/v1/chat/completions")
MODEL = os.environ.get("LLM_MODEL", "llama3.2")
def ask(question: str) -> str:
payload = {
"model": MODEL,
"temperature": 0.2,
"messages": [
{"role": "system", "content": "You are a concise Python expert."},
{"role": "user", "content": question},
],
}
headers = {"Content-Type": "application/json"}
if key := os.environ.get("LLM_API_KEY"): # hosted providers
headers["Authorization"] = f"Bearer {key}"
req = urllib.request.Request(ENDPOINT, data=json.dumps(payload).encode(), headers=headers)
with urllib.request.urlopen(req, timeout=120) as resp:
data = json.load(resp)
return data["choices"][0]["message"]["content"]
if __name__ == "__main__":
print(ask(" ".join(sys.argv[1:]) or "Explain Python decorators in 3 sentences."))
Compare it with the Scala version on page 10. The request shape is identical. That is the value of a common API shape.
Everyday Python + AI wins
- Throwaway automation: "Write a script that reads all
*.logfiles, extracts ERROR lines with timestamps, and outputs a CSV summary per hour." Review it, run it, delete it. - Data exploration: in a notebook, ask the assistant to load a CSV, profile it, and suggest data-quality checks.
- Glue code: converting formats (JSON ↔ YAML ↔ CSV), calling REST APIs, generating test fixtures.
- Learning: "I know Scala. Explain Python's iterators/generators by comparing them to Scala's
IteratorandLazyList."
| Scala concept | Closest Python equivalent |
|---|---|
case class | @dataclass(frozen=True), or pydantic models |
enum / sealed trait ADT | enum.Enum, or a union of dataclasses + match |
Option[A] | A | None (checked by mypy) |
Either[E, A] | Exceptions (idiomatic), or a Result type library |
| for-comprehension | list/generator comprehensions |
LazyList / Iterator | generators (yield) |
Type classes / given | Protocols (typing.Protocol), functools.singledispatch |
Cats Effect IO | asyncio coroutines (different model) |
- Create a project with
uv init, add pytest/hypothesis/ruff/mypy. - Use the
parse_durationprompt above with your IDE agent and tell it to run tests until they pass. - Run
ruffandmypy --strict. How many issues did the AI leave? Ask it to fix them. - Run
ask_llm.pyagainst a local model (install Ollama) or a hosted one.
Key takeaways
- Python is the lingua franca of AI tooling, so it is worth knowing even if your main language is Scala.
- uv + ruff + mypy + pytest/hypothesis give agents fast, automatic feedback.
- State version, typing level and library versions. Watch for the classic Python and security mistakes.
- Verify unfamiliar packages before installing. Hallucinated packages are an attack vector.