Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Note: This is a quick-reference Python guide focused on AI/ML workflows. For a comprehensive Python course, see our Complete Python Crash Course.
Getting Started
1. Install Python
Download Python 3.11+ from python.org. Verify installation:
python --version # Should show 3.11 or higher
2. Set Up Virtual Environment
Virtual environments are like separate toolboxes for each project. Without them, installing a package for Project A might break Project B because they need different versions of the same library. This is not a theoretical concern — it will happen to you within your first week of AI development, because LLM libraries update frequently and often have conflicting dependencies.
# Create a virtual environment
python -m venv venv
# Activate (Windows)
venv\Scripts\activate
# Activate (macOS/Linux)
source venv/bin/activate
3. Install AI Packages
pip install openai anthropic langchain chromadb pydantic python-dotenv
4. Manage Dependencies
# Save current packages
pip freeze > requirements.txt
# Install from requirements
pip install -r requirements.txt
Python Core Syntax (AI Context)
These are the Python fundamentals you will use daily in AI engineering. We focus on what matters for working with LLM APIs, data processing, and async workflows — not the full breadth of Python.
Variables & Types
# Basic types -- you will use all four of these in every LLM API call
name = "Claude" # str: model names, prompts, responses
temperature = 0.7 # float: model parameters, scores, costs
max_tokens = 1000 # int: token limits, counts, retry attempts
is_streaming = True # bool: feature flags, configuration
# Lists (ordered, mutable)
messages = ["Hello", "How are you?"]
messages.append("Great!")
# Dictionaries -- the most important data structure for AI work.
# Every API request and response is essentially a dict.
config = {
"model": "gpt-4",
"temperature": 0.7,
"max_tokens": 1000
}
# Tuples (immutable)
coordinates = (40.7128, -74.0060)
Functions
def generate_prompt(context: str, question: str) -> str:
"""Generate a prompt with context and question."""
return f"Context: {context}\n\nQuestion: {question}"
# With default args
def call_api(prompt: str, temperature: float = 0.7, max_tokens: int = 1000):
# API call logic here
pass
# Lambda (anonymous functions)
multiply = lambda x, y: x * y
result = multiply(5, 3) # 15
Control Flow
# If-else
if temperature < 0.3:
style = "deterministic"
elif temperature < 0.7:
style = "balanced"
else:
style = "creative"
# For loops
for message in messages:
print(message)
# List comprehension (faster, more Pythonic)
lengths = [len(msg) for msg in messages]
filtered = [msg for msg in messages if len(msg) > 10]
# While loop
retry_count = 0
while retry_count < 3:
try:
# Try API call
break
except Exception:
retry_count += 1
Error Handling
Error handling is not optional in AI engineering — LLM APIs fail regularly due to rate limits, network issues, and content policy violations. Every API call should be wrapped in try/except. The pattern below catches errors from most specific to most general, which is important because Python matches the first except block that fits.
try:
response = api_call(prompt)
except APIError as e:
# Catch specific API errors first (rate limits, invalid requests, etc.)
print(f"API error: {e}")
except TimeoutError:
# Network timeouts are common with LLM APIs (long generation times)
print("Request timed out")
finally:
# Always runs, even if an exception was raised -- use for cleanup
close_connection()
Data Structures for AI
Working with JSON
JSON is the lingua franca of LLM APIs. Every request you send is JSON, every response you receive is JSON, and structured outputs are JSON. Mastering json.loads() and json.dumps() is as fundamental to AI engineering as knowing how to read and write.
import json
# Parse JSON string -- turns a string into a Python dict
data = json.loads('{"model": "gpt-4", "temp": 0.7}')
# Convert to JSON string
json_str = json.dumps({"result": "success"})
# Read from file
with open("config.json") as f:
config = json.load(f)
# Write to file
with open("output.json", "w") as f:
json.dump(results, f, indent=2)
List Operations
# Slicing
messages[0] # First item
messages[-1] # Last item
messages[1:3] # Items 1-2
messages[:2] # First 2 items
messages[2:] # From item 2 to end
# Common operations
len(messages) # Length
messages.extend([...]) # Add multiple items
messages.remove(item) # Remove specific item
messages.pop() # Remove & return last item
# Sorting
sorted_list = sorted(numbers)
messages.sort(key=lambda x: len(x)) # Sort by length
Dictionary Operations
# Access
value = config["model"]
value = config.get("model", "default") # Safe access
# Check existence
if "model" in config:
print(config["model"])
# Iteration
for key, value in config.items():
print(f"{key}: {value}")
# Merge dictionaries
combined = {**config1, **config2}
# Dictionary comprehension
squares = {x: x**2 for x in range(5)}
Object-Oriented Python for AI
Classes & Dataclasses
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class Message:
role: str
content: str
tokens: Optional[int] = None
class ChatBot:
def __init__(self, model: str = "gpt-4"):
self.model = model
self.messages: List[Message] = []
def add_message(self, role: str, content: str) -> None:
msg = Message(role=role, content=content)
self.messages.append(msg)
def get_history(self) -> list[dict]:
return [
{"role": m.role, "content": m.content}
for m in self.messages
]
def clear(self) -> None:
self.messages = []
# Usage
bot = ChatBot(model="gpt-4o")
bot.add_message("user", "Hello!")
bot.add_message("assistant", "Hi there!")
print(bot.get_history())
Why dataclasses? Reduces boilerplate for data objects. Perfect for API responses, configuration objects, and structured data.
Type Hints (Modern Python)
from typing import List, Dict, Optional, Union
def process_batch(
items: List[str],
config: Dict[str, any],
timeout: Optional[float] = None
) -> List[Dict[str, Union[str, int]]]:
"""Process a batch of items with config."""
results = []
for item in items:
result = {"text": item, "length": len(item)}
results.append(result)
return results
Why type hints? Better IDE support, catch bugs early, and self-documenting code.
Dependency Management: pip vs. Poetry vs. uv
Choosing the right tool for managing Python packages will save you hours of debugging dependency conflicts — a common occurrence in AI projects because libraries like langchain, transformers, and torch have deep and sometimes conflicting dependency trees.
| Tool | Speed | Lock File | Best For | Watch Out For |
|---|
| pip + requirements.txt | Slow | Manual (pip freeze) | Simple projects, tutorials | No true dependency resolution; pip freeze captures everything including transitive deps |
| pip + pip-tools | Moderate | requirements.in -> requirements.txt | Production projects needing reproducibility | Extra step to compile lock file |
| Poetry | Moderate | poetry.lock (automatic) | Libraries, projects needing publishing | Slow resolver, can conflict with conda |
| uv | Very fast (10-100x pip) | uv.lock (automatic) | New projects in 2025+, fast iteration | Newer tool, some edge cases with exotic packages |
| conda | Slow | environment.yml | Data science, GPU/CUDA deps, cross-platform binaries | Heavy, dependency resolution can be painfully slow |
AI-specific recommendation: For AI projects that need PyTorch or CUDA, start with uv (fast, modern) and fall back to conda only if you need binary packages that pip cannot install (e.g., specific CUDA toolkit versions). For everything else, uv or pip-tools gives you speed and reproducibility.
Advanced Patterns for AI Engineering
Decorators (Reusable Logic)
Decorators are functions that wrap other functions to add behavior — think of them as “middleware for functions.” They are everywhere in AI engineering: @retry for handling flaky API calls, @timer for profiling, @cache for avoiding redundant LLM calls, and @observe for tracing. If you understand decorators, you can read (and write) production AI code. If you do not, they will look like magic.
import functools
import time
from typing import Callable
def timer(func: Callable) -> Callable:
"""Measure execution time"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
duration = time.time() - start
print(f"{func.__name__} took {duration:.2f}s")
return result
return wrapper
def retry(max_attempts: int = 3, delay: float = 1.0):
"""Retry decorator with exponential backoff"""
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise
wait_time = delay * (2 ** attempt)
print(f"Retry {attempt + 1}/{max_attempts} in {wait_time}s")
time.sleep(wait_time)
return wrapper
return decorator
def cache_result(func: Callable) -> Callable:
"""Simple caching decorator"""
cache = {}
@functools.wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
# Usage
@retry(max_attempts=3, delay=2.0)
@timer
def call_llm(prompt: str) -> str:
# Simulated API call
return "response"
Use cases:
@timer - Profile slow functions
@retry - Handle flaky API calls
@cache_result - Avoid redundant LLM calls
Context Managers (Resource Management)
Context managers ensure resources are properly managed—files closed, connections released, timers stopped.
from contextlib import contextmanager
import time
@contextmanager
def timer_context(label: str):
"""Time a block of code"""
start = time.time()
try:
yield
finally:
duration = time.time() - start
print(f"{label}: {duration:.2f}s")
@contextmanager
def temporary_config(new_config: dict):
"""Temporarily change config, then restore"""
old_config = config.copy()
config.update(new_config)
try:
yield
finally:
config.clear()
config.update(old_config)
# Usage
with timer_context("Embedding generation"):
embeddings = generate_embeddings(texts)
with open("data.txt") as f:
content = f.read()
Use cases:
- File I/O
- Database connections
- Timing code blocks
- Temporary state changes
Async/Await (Concurrency)
Async is the single most important advanced Python pattern for AI engineering. Here is why: a typical LLM API call takes 1-5 seconds, and during that time your program is just waiting for a network response. Without async, processing 10 prompts takes 10-50 seconds. With async, all 10 run concurrently and you get results in 1-5 seconds total. That is a 10x speedup for free.
The mental model: async def declares a function that can pause (at await points) and let other tasks run while it waits. asyncio.gather runs multiple async tasks concurrently.
import asyncio
from typing import List
async def fetch_completion(prompt: str) -> str:
"""Simulated async API call -- the await lets other tasks run while waiting"""
await asyncio.sleep(1) # In real code, this is the LLM API call
return f"Response to: {prompt}"
async def process_batch(prompts: List[str]) -> List[str]:
"""Process multiple prompts concurrently.
asyncio.gather runs all tasks at once and waits for all to complete.
Order is preserved: results[0] corresponds to prompts[0].
"""
tasks = [fetch_completion(p) for p in prompts]
results = await asyncio.gather(*tasks)
return results
# asyncio.run() is the entry point -- call it once from synchronous code
prompts = ["Question 1", "Question 2", "Question 3"]
results = asyncio.run(process_batch(prompts))
Why async? Process multiple API calls concurrently. 10 sequential 1-second calls = 10 seconds. 10 concurrent = ~1 second. For batch processing, this is not a nice-to-have — it is the difference between a feature that ships and one that times out.
File Operations
# Read entire file
with open("data.txt") as f:
content = f.read()
# Read line by line (memory efficient)
with open("large_file.txt") as f:
for line in f:
process(line.strip())
# Write to file
with open("output.txt", "w") as f:
f.write("Hello, world!\n")
# Append to file
with open("log.txt", "a") as f:
f.write(f"{timestamp}: Event\n")
# Check if file exists
from pathlib import Path
if Path("config.json").exists():
# Load config
pass
Environment Variables (.env)
API keys are the crown jewels of your AI application. A leaked OpenAI key can rack up thousands of dollars in charges before you notice. The .env pattern keeps secrets out of your code and out of git history. This is not a suggestion — it is a hard requirement for any project that will ever be shared, deployed, or committed to a repository.
from dotenv import load_dotenv
import os
# Load from .env file -- call this once at startup
load_dotenv()
# Access variables -- os.getenv returns None if not found (no crash)
api_key = os.getenv("OPENAI_API_KEY")
db_url = os.getenv("DATABASE_URL", "default_url") # Second arg is fallback
.env file:
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
DATABASE_URL=postgresql://...
Never commit .env files! Add .env to your .gitignore immediately when you create a new project — before you make your first commit. If you accidentally commit a key, rotate it immediately; removing it from git history is difficult and unreliable.
Essential Libraries for AI
HTTP Requests
import requests
# GET request
response = requests.get("https://api.example.com/data")
data = response.json()
# POST request
response = requests.post(
"https://api.example.com/generate",
json={"prompt": "Hello", "max_tokens": 100},
headers={"Authorization": f"Bearer {api_key}"}
)
Data Manipulation (Pandas)
import pandas as pd
# Read CSV
df = pd.read_csv("data.csv")
# Basic operations
df.head() # First 5 rows
df.describe() # Statistics
df["column"].mean() # Column average
# Filter
filtered = df[df["score"] > 0.8]
# Group by
grouped = df.groupby("category")["score"].mean()
Date & Time
from datetime import datetime, timedelta
now = datetime.now()
timestamp = now.isoformat()
# Add time
tomorrow = now + timedelta(days=1)
hour_ago = now - timedelta(hours=1)
# Parse string
dt = datetime.fromisoformat("2024-01-15T10:30:00")
Common AI Patterns
These patterns appear in virtually every AI application. They are worth memorizing because you will use them dozens of times.
Loading Environment Variables
from dotenv import load_dotenv
import os
load_dotenv()
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
if not OPENAI_API_KEY:
raise ValueError("OPENAI_API_KEY not set")
Building Prompts
def build_rag_prompt(context: str, question: str) -> str:
"""Build RAG prompt with context."""
return f"""Use the following context to answer the question.
Context:
{context}
Question: {question}
Answer:"""
# Template with f-strings
system_prompt = f"""You are an AI assistant with expertise in {domain}.
Your responses should be {tone} and {length}."""
Batching Requests
Batching is essential when you have hundreds or thousands of items to process. Sending them all at once will hit rate limits; sending them one at a time is painfully slow. Batching gives you the best of both worlds: controlled throughput that stays within API limits while processing efficiently. The yield keyword makes this a generator, which means it processes one batch at a time and does not load all results into memory.
def batch_process(items: List[str], batch_size: int = 10):
"""Process items in batches to stay within rate limits."""
for i in range(0, len(items), batch_size):
batch = items[i:i + batch_size]
results = process_batch(batch)
yield results # yield returns results one batch at a time (memory-efficient)
Rate Limiting
OpenAI, Anthropic, and other providers enforce rate limits (typically measured in requests per minute and tokens per minute). Exceeding them results in 429 errors and temporary bans. This simple rate limiter adds a fixed delay between calls to keep you under the limit. For production use, consider the tenacity library for more sophisticated retry-with-backoff patterns.
import time
def rate_limited_call(func, calls_per_minute: int = 60):
"""Rate limit function calls.
Simple but effective: adds a fixed delay between calls.
For 60 calls/min, that is 1 second between each call.
"""
delay = 60.0 / calls_per_minute
def wrapper(*args, **kwargs):
time.sleep(delay) # Pause before each call to stay under the limit
return func(*args, **kwargs)
return wrapper
Sync vs. Async: When to Use What
| Scenario | Use Sync | Use Async | Why |
|---|
| Single LLM call | Yes | No | No concurrency benefit; simpler code |
| Batch of 10+ LLM calls | No | Yes | 10x speedup from concurrent I/O |
| FastAPI endpoint | Either | Preferred | FastAPI is async-native; mixing sync blocks the event loop |
| Jupyter notebook | Yes | Tricky | Notebooks already run an event loop; asyncio.run() will fail — use await directly or nest_asyncio |
| CLI script | Yes | Yes (for batch) | Sync is simpler; use async only if you have batch processing |
| Streaming response | Either | Preferred | Async generators (async for) integrate cleanly with streaming APIs |
Edge case — mixing sync and async: If you call a sync function (like requests.get()) from inside an async function, it blocks the entire event loop. Use httpx (async HTTP) instead of requests, or wrap sync calls in asyncio.to_thread() to run them in a thread pool without blocking.
Next Steps
Next Steps
Quick Reference
Common Commands
# Python version
python --version
# Install package
pip install package-name
# Install from requirements
pip install -r requirements.txt
# Create requirements file
pip freeze > requirements.txt
# Create virtual environment
python -m venv venv
# Activate venv (Windows)
venv\Scripts\activate
# Activate venv (macOS/Linux)
source venv/bin/activate
# Deactivate venv
deactivate
# Run Python file
python script.py
# Interactive Python shell
python
# Install specific version
pip install package-name==1.2.3
Style Guidelines (PEP 8)
# Naming conventions
class MyClass: # PascalCase for classes
pass
def my_function(): # snake_case for functions/variables
pass
CONSTANT_VALUE = 100 # UPPER_CASE for constants
# Line length: max 79-88 characters
# Imports at top, grouped: standard lib, third-party, local
# Use 4 spaces for indentation (not tabs)
# Two blank lines between top-level functions/classes
# One blank line between methods
Type Hints Quick Reference
from typing import List, Dict, Optional, Union, Callable, Any
def func(
text: str, # String
count: int, # Integer
ratio: float, # Float
is_valid: bool, # Boolean
items: List[str], # List of strings
config: Dict[str, int], # Dict with str keys, int values
callback: Callable[[str], int], # Function: str -> int
optional: Optional[str] = None, # Can be str or None
either: Union[str, int], # Can be str OR int
anything: Any # Any type
) -> tuple[str, int]: # Returns tuple
return ("result", 42)
Common Python Gotchas in AI Work
These are the mistakes that burn hours of debugging time specifically in AI engineering contexts:
| Gotcha | What Happens | Fix |
|---|
| Mutable default arguments | def f(items=[]) shares the same list across all calls — appended items persist | Use def f(items=None): items = items or [] |
| Shallow copy of dicts | config2 = config1 means both point to the same dict; changing one changes both | Use config2 = config1.copy() or copy.deepcopy() for nested dicts |
| f-string with dicts | f"value: {d['key']}" fails with single quotes inside f-string braces | Use double quotes: f"value: {d[\"key\"]}" or assign to variable first |
JSON dumps vs dump | json.dumps() returns a string; json.dump() writes to a file. Mixing them up produces cryptic errors | Remember: the s stands for “string” |
asyncio.run() in Jupyter | Raises RuntimeError: cannot run nested event loop | Use await directly in cells, or pip install nest_asyncio; nest_asyncio.apply() |
| Float precision in cost calculations | 0.1 + 0.2 == 0.30000000000000004 | Use round() or Decimal for financial calculations |
Forgetting await | result = async_func() returns a coroutine object, not the result | Always result = await async_func() — linters catch this if you use type hints |
Troubleshooting
”Module not found” error
# Make sure venv is activated
# Then reinstall
pip install -r requirements.txt
”pip: command not found”
# Use python -m pip instead
python -m pip install package-name
Import errors in VS Code
- Select correct Python interpreter:
Ctrl+Shift+P → “Python: Select Interpreter”
- Choose the one in your
venv folder
Slow pip installs
# Use a faster mirror
pip install --index-url https://pypi.org/simple package-name
Pro Tips:
- Use virtual environments for EVERY project
- Pin package versions in production (
package==1.2.3)
- Use type hints—they catch bugs before runtime
- Learn list/dict comprehensions—they’re faster and more Pythonic
- Use
python-dotenv for API keys and secrets