Python Basics — Python for Scripting and AI
Status: Active | Last Updated: 2026-09-15 Category: Languages — Python Prerequisites: Basic programming concepts (see kb/basics/) Tags: python, venv, pip, scripting, ai Estimated Time: 4-6 hours (Self-paced, includes lab time)
Summary
Python is the language of choice for AI/ML, scripting, and glue code. It uses indentation for block structure (no braces) and prioritizes readability. This article covers syntax, data structures, virtual environments, and the standard library modules you will use most often. By the end, you will be able to write, run, and distribute real Python scripts and understand why Python dominates the AI/ML ecosystem.
What You'll Learn (Core Competencies)
- Write idiomatic Python using indentation-based block structure and Pythonic idioms
- Use the core built-in data structures (list, dict, set, tuple) and choose the right one for a given access pattern
- Define functions with type hints and classes with
__init__, properties, and inheritance - Create and manage isolated virtual environments with
venvand pin dependencies withpip - Use the most common stdlib modules (
os,json,argparse,datetime,pathlib,subprocess)
Table of Contents
- Architectural Overview & Core Schema
- Deep Dive & Implementation
- Anti-Patterns & Common Pitfalls
- Independent Challenge
- Consolidation & Key Invariants
- Next Steps
1. Architectural Overview & Core Schema
Why Python
Python is interpreted (no compile step), dynamically typed (variables have types but you do not declare them), and used heavily in:
- AI/ML: PyTorch, TensorFlow, LangChain
- Scripting: replace bash for complex logic
- Data: pandas, NumPy
- Web: Django, FastAPI, Flask
The Interpretation Model
Python's execution model is distinct from compiled languages:
| Layer | What Exists | Examples |
|---|---|---|
| Source | Python with indentation | def greet(name: str) -> str: |
| Compile-time | Python compiler parses to bytecode | .pyc files in __pycache__/ |
| Runtime | CPython VM executes bytecode | Python integers are arbitrary-precision |
Because Python is interpreted, there is no separate compilation step and no separate build artifact to ship. You ship the .py source (or bundle it with PyInstaller/Nuitka). The interpreter reads and executes source line by line, though CPython does compile to bytecode internally for performance.
Type Hints as Documentation and Tooling
Python 3.5 introduced type hints (PEP 484). They do not affect runtime behavior, but they are read by IDEs (autocomplete, error highlighting), static analyzers (mypy, pyright), and formatters (ruff). Type hints make code self-documenting and enable tooling that catches type mismatches before runtime.
def greet(name: str, count: int = 1) -> str:
"""Return a greeting string."""
return ", ".join([f"Hello, {name}"] * count)
Without type hints, the function signature is opaque to tooling. With them, mypy can catch greet(42) before the code runs.
The GIL and Concurrency
Python has a Global Interpreter Lock (GIL), which allows only one thread to execute Python bytecode at a time. This simplifies memory management but limits CPU-bound parallelism. For I/O-bound tasks, asyncio (single-threaded async) or threading (useful for some I/O-bound tasks despite the GIL) are appropriate. For CPU-bound tasks, use multiprocessing (separate processes, each with its own GIL) or native C extensions. The GIL is irrelevant for I/O-bound workloads because the interpreter releases it during I/O waits.
2. Deep Dive & Implementation
Syntax Basics
Variables and Basic Types
Python variables spring into existence when you assign to them. There is no declaration step and no type annotation required, though type hints are available in Python 3.5+ for documentation and tooling. The interpreter infers the type from the right-hand side expression, and that type can change if you reassign the variable:
name = "fogserv" # str
count = 42 # int
ratio = 3.14 # float
enabled = True # bool
nothing = None # NoneType (null-like)
Python integers have arbitrary precision, meaning count = 42 behaves identically to count = 10**100. Floats follow IEEE 754 double-precision, same as JavaScript's number type. Strings are immutable Unicode sequences. There is no separate character type — a single character is just a string of length 1.
Booleans are True and False (capitalized). Most objects have a truthiness: 0, "", [], {}, None evaluate to False; everything else is True. This makes conditionals concise:
items = []
if items: # False — empty list
print("has items")
Indentation as Block Structure
Python uses indentation instead of braces to delineate blocks. This is not a stylistic suggestion — the interpreter enforces it. A logical block (function body, if branch, class definition) is defined by consistent indentation. The official style guide (PEP 8) recommends 4 spaces per indentation level, and most tools enforce this. Mixing tabs and spaces causes an IndentationError in Python 3.
def classify(count: int) -> str:
if count > 100:
return "very many"
elif count > 10:
return "many"
else:
return "few"
The -> str and : int annotations above are type hints. They do not affect runtime behavior but are read by IDEs, mypy, and other static analysis tools. Always prefer 4 spaces. Never use tabs. Configure your editor to insert spaces when you press Tab.
Control Flow
Python's if/elif/else works as expected, but the elif keyword replaces the else if pattern from C/Java. The match/case statement (Python 3.10+) provides structural pattern matching:
status = "running"
match status:
case "running":
print("Service is up")
case "stopped":
print("Service is down")
case _:
print("Unknown status")
Loops use for and while. The for loop iterates over any iterable (list, string, file, range, etc.). The range() function generates sequences without materializing them in memory:
for i in range(10): # 0 to 9
print(i)
for char in "fogserv":
print(char)
Use enumerate() when you need both index and value. Use zip() to iterate over multiple sequences in parallel:
servers = ["alpha", "beta", "gamma"]
ports = [3000, 3001, 3002]
for i, (name, port) in enumerate(zip(servers, ports)):
print(f"{i}: {name} on port {port}")
The break and continue statements work as in other languages. The else clause on loops runs only if the loop completes without hitting break — a useful pattern for search algorithms:
for n in range(2, 20):
for d in range(2, n):
if n % d == 0:
break
else:
print(n, "is prime")
String Formatting
Python offers four ways to format strings. F-strings (formatted string literals, Python 3.6+) are the preferred modern style. They embed expressions inside {} with optional format specs:
name = "fogserv"
port = 3000
info = f"{name} running on port {port}" # simple interpolation
width = f"{port:05d}" # zero-padded to 5 digits
price = f"${3.14159:.2f}" # 2 decimal places
The .format() method (Python 2.6+) uses positional or named placeholders. The %-formatting style (like sprintf) is the oldest and still common in legacy code:
"Hello, {}".format(name) # .format() style
"Hello, %s" % name # % style (legacy)
For machine-readable output (logging, APIs), prefer f-strings. For user-facing text with localization needs, use the gettext module or a higher-level i18n framework.
Data Structures
Lists
Lists are ordered, mutable sequences. They are the workhorse data structure in Python — used where arrays, stacks, queues, and dynamic arrays would be used in other languages. Lists are created with square brackets or the list() constructor:
stack = []
stack.append("first")
stack.append("second")
top = stack.pop() # "second"
queue = []
queue.append("a")
queue.append("b")
first = queue.pop(0) # "a"
List comprehensions provide a concise, Pythonic way to create lists from iterables. They combine a loop and element transformation in a single expression and are typically faster than equivalent for loops with .append():
squares = [x**2 for x in range(10)] # [0, 1, 4, 9, 16, ...]
evens = [x for x in range(20) if x % 2 == 0]
pairs = [(a, b) for a in range(3) for b in range(3)]
Lists support slicing with list[start:stop:step]. Omitting a boundary means "from start" or "to end". Slicing returns a new list:
data = [0, 10, 20, 30, 40, 50]
data[1:4] # [10, 20, 30]
data[:3] # [0, 10, 20]
data[-2:] # [40, 50]
data[::2] # [0, 20, 40] — every other element
data[::-1] # [50, 40, 30, 20, 10, 0] — reversed
Dictionaries
Dictionaries (dict) store key-value pairs with O(1) average-case lookup by hashable key. Keys must be immutable (str, int, tuple, etc.). Dicts preserve insertion order (since Python 3.7+, this is a language guarantee, not just an implementation detail):
config = {
"host": "0.0.0.0",
"port": 3000,
"debug": False,
}
config["port"] = 8080 # update
config["timeout"] = 30 # insert
value = config.get("missing", "default") # safe access with fallback
for key, value in config.items():
print(f"{key} = {value}")
Dictionary comprehensions mirror list comprehensions:
squares = {k: k**2 for k in range(5)} # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
Sets
Sets are unordered collections of unique, hashable elements. They support fast membership testing (O(1)), union, intersection, and difference operations. Use sets when you need to track unique values or test set membership:
tags = {"python", "cli", "automation"}
tags.add("scripting")
tags.remove("cli")
if "python" in tags:
print("Python is in the set")
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
print(a & b) # {3, 4} — intersection
print(a | b) # {1, 2, 3, 4, 5, 6} — union
print(a - b) # {1, 2} — difference
Tuples
Tuples are immutable, ordered sequences. They serve two main roles: as fixed-length records (like lightweight structs) and as dict keys (since immutable). Unlike lists, tuples are hashable if all their elements are hashable:
point = (10, 20)
x, y = point # unpacking
# Named tuple for clearer record access
from collections import namedtuple
Server = namedtuple("Server", ["name", "host", "port"])
srv = Server("prod-1", "10.0.0.5", 3000)
print(srv.name) # "prod-1"
Because tuples are immutable, they can serve as dictionary keys or be placed in sets, whereas lists cannot. Use tuples for fixed-length heterogeneous sequences and lists for variable-length homogeneous ones.
When to Use Which
Choose based on your access pattern: indexed by position (list/tuple), indexed by key (dict), unique membership (set), fixed record (tuple or namedtuple). A common beginner mistake is using a list where a dict or set is more appropriate. If you find yourself writing for item in items: if item.key == value, consider a dict keyed by item.key instead.
Functions and Classes
Defining and Calling Functions
Functions are defined with def, can accept positional and keyword arguments, and return values with return. Python supports default argument values, *args for variadic positionals, and **kwargs for variadic keyword arguments:
def greet(name: str, greeting: str = "Hello") -> str:
"""Return a greeting string. Docstrings go triple-quoted."""
return f"{greeting}, {name}!"
def fetch_all(*urls: str) -> list[str]:
results = []
for url in urls:
results.append(fetch_one(url))
return results
def configure(**settings: str | int) -> dict:
return {k.upper(): v for k, v in settings.items()}
Python functions are first-class objects: they can be assigned to variables, passed as arguments, and returned from other functions. This enables powerful functional programming patterns:
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
sorted_nums = sorted(numbers, key=lambda x: -x) # descending
Classes and Objects
Python's class system is single-inheritance with multiple interface inheritance through Abstract Base Classes (ABCs). The __init__ method is the initializer (not the constructor — __new__ handles that). Instance variables are created by assignment in __init__ or any method:
class Server:
def __init__(self, name: str, host: str, port: int):
self.name = name # public attribute
self._host = host # convention: "protected"
self.__port = port # name-mangled to _Server__port
@property
def address(self) -> str:
return f"{self._host}:{self.__port}"
def start(self) -> None:
print(f"Starting {self.name} at {self.address}")
srv = Server("prod", "0.0.0.0", 3000)
srv.start()
print(srv.address) # calls the property method
Methods marked with @property look like attribute access but execute code. This is the Pythonic way to expose computed attributes. Use @staticmethod for methods that do not need self, and @classmethod when a method needs to create an instance via an alternate constructor:
class Config:
def __init__(self, data: dict):
self._data = data
@classmethod
def from_file(cls, path: str) -> "Config":
import json
with open(path) as f:
return cls(json.load(f))
Inheritance and MRO
Python supports single inheritance and multiple inheritance via the Method Resolution Order (MRO), which follows C3 linearization. Use super() to call the parent class method, which correctly traverses the MRO:
class BaseServer:
def start(self) -> None:
print("Starting base")
class HTTPServer(BaseServer):
def start(self) -> None:
print("Starting HTTP server")
super().start() # call parent's start()
Modules and Imports
Every .py file is a module. Packages are directories with an __init__.py. Import with import module or from module import name. Avoid from module import * — it pollutes the namespace. Use absolute imports (from the project root) or explicit relative imports:
# absolute import
from kb.runtime import bun_runtime
# relative import (inside a package)
from . import utils
from ..languages import python_basics
The __all__ list in a module controls what from module import * exports. Always define __all__ if you expect others to use star imports.
Virtual Environments
Why Virtual Environments
Python's global site-packages directory is shared across all projects. Installing a library for one project can silently break another project that depends on a different version of the same library. Virtual environments solve this by creating isolated Python installations, each with their own site-packages directory.
Creating and Managing venv
The venv module is part of the standard library (Python 3.3+). Create an environment with:
# Create a virtual environment named .venv in the current directory
python3 -m venv .venv
# Activate it (Linux/macOS)
source .venv/bin/activate
# Activate it (Windows PowerShell)
.venv\Scripts\Activate.ps1
# Now your prompt shows (.venv) and pip points to the isolated env
pip install requests httpx
When the environment is active, pip install installs into .venv/lib/python3.x/site-packages/, not the global site-packages. Deactivate with the deactivate command.
pip and Requirements Files
pip is the package installer. It downloads packages from PyPI (Python Package Index) by default. Pin your dependencies to specific versions to ensure reproducible builds:
pip freeze > requirements.txt # export current env's packages
pip install -r requirements.txt # install from file
The output of pip freeze includes every package, including transitive dependencies. For cleaner dependency files, consider pip-tools with its pip-compile command, which resolves dependencies and produces a locked requirements file:
# requirements.in — human-written, loose deps
# package>=1.0
# another-package
pip-compile requirements.in # produces requirements.txt with pinned versions
pyproject.toml (Modern Standard)
Modern Python projects use pyproject.toml (PEP 517/518) instead of setup.py or requirements.txt for build system configuration. The hatch, poetry, and pip-tools ecosystems all respect this format:
[project]
name = "fogserv-agent"
version = "0.1.0"
dependencies = [
"httpx>=0.27.0",
"pydantic>=2.0",
]
[project.optional-dependencies]
dev = ["pytest>=8.0", "ruff>=0.5"]
With this setup, pip install -e ".[dev]" installs the package in editable mode with dev dependencies. Use ruff or black for formatting and mypy or pyright for type checking in CI.
Common Stdlib
os — Operating System Interface
The os module provides portable access to OS services. Always prefer pathlib.Path for path manipulation over string concatenation:
import os
from pathlib import Path
# Legacy os.path style
config_path = os.path.join(os.path.expanduser("~"), ".config", "app")
# Modern pathlib style
config_dir = Path.home() / ".config" / "app"
config_dir.mkdir(parents=True, exist_ok=True)
(config_dir / "settings.json").write_text(json.dumps(settings))
Walk a directory tree with os.walk or the Path.rglob() method for glob-based traversal:
for py_file in Path("kb").rglob("*.md"):
print(py_file)
json — JSON Encoding and Decoding
JSON is the lingua franca of web APIs. Python's json module serializes Python objects to JSON strings and back:
import json
data = {"name": "fogserv", "ports": [3000, 3001]}
json_str = json.dumps(data, indent=2) # dict → JSON string
parsed = json.loads(json_str) # JSON string → dict
# Work with files directly
with open("config.json") as f:
config = json.load(f) # file → dict
with open("out.json", "w") as f:
json.dump(config, f, indent=2) # dict → file
For large JSON files, use ijson (streaming parser) or orjson (faster C-based encoder).
argparse — Command-Line Argument Parsing
For CLI scripts, argparse builds structured argument parsers with help text, type coercion, and subcommand support:
import argparse
parser = argparse.ArgumentParser(description="Deploy a service")
parser.add_argument("name", help="Service name")
parser.add_argument("--port", type=int, default=3000, help="Port number")
parser.add_argument("--verbose", "-v", action="store_true")
args = parser.parse_args()
if args.verbose:
print(f"Deploying {args.name} on port {args.port}")
Use argparse for any script that accepts arguments. For complex CLIs with subcommands, use click or typer instead — they build on top of argparse but offer a decorator-based interface.
datetime — Dates and Times
Python's datetime module handles dates, times, and their combinations. Always use timezone-aware datetimes in production:
from datetime import datetime, timezone
# UTC-aware current time
now = datetime.now(timezone.utc)
# Parse ISO 8601 string
timestamp = datetime.fromisoformat("2026-09-15T10:30:00Z")
# Format for display or storage
print(now.strftime("%Y-%m-%d %H:%M:%S %Z"))
For timezone conversions, use zoneinfo.ZoneInfo (Python 3.9+). For date arithmetic, use timedelta. Avoid naive datetimes (without timezone) in server code — they are a common source of off-by-one bugs in production.
pathlib — Object-Oriented Filesystem Paths
pathlib (Python 3.4+) replaces os.path and os file functions with an object-oriented API. Path objects represent filesystem paths and expose methods for reading, writing, globbing, and directory traversal:
from pathlib import Path
p = Path("kb/runtime")
p.exists() # True if path exists
p.is_dir() # True if directory
list(p.glob("*.md")) # all .md files in runtime/
# Read/write files
content = Path("README.md").read_text()
Path("out.txt").write_text("hello")
Prefer pathlib over open() for simple reads/writes and over os.path.join for path construction. It is more readable and composable.
subprocess — Running External Commands
For running shell commands from Python, use subprocess.run() (Python 3.5+). Avoid os.system() and subprocess.call() — they are older and less flexible:
import subprocess
result = subprocess.run(
["git", "log", "--oneline", "-5"],
capture_output=True,
text=True,
check=True, # raise CalledProcessError on non-zero exit
)
print(result.stdout)
For long-running processes, use subprocess.Popen for streaming I/O or the asyncio module with asyncio.create_subprocess_exec for async execution.
Guided Checkpoint
Create a virtual environment, install a package, and run a script that uses argparse, pathlib, and datetime:
# Create a fresh venv and install httpx
python3 -m venv /tmp/py-checkpoint
source /tmp/py-checkpoint/bin/activate
pip install httpx --quiet
# Write and run a small CLI script
cat > /tmp/py-checkpoint/fetch.py <<'EOF'
import argparse, sys
from pathlib import Path
from datetime import datetime, timezone
parser = argparse.ArgumentParser(description="Fetch a URL and log metadata")
parser.add_argument("url", help="URL to fetch")
parser.add_argument("--out", default="response.txt", help="Output file")
args = parser.parse_args()
import httpx
with httpx.Client() as client:
r = client.get(args.url)
Path(args.out).write_text(f"[{datetime.now(timezone.utc).isoformat()}] {r.status_code} {len(r.content)} bytes\n")
print(f"Wrote {Path(args.out).stat().st_size} bytes to {args.out}")
EOF
# Run with a valid URL
python /tmp/py-checkpoint/fetch.py https://httpbin.org/get --out /tmp/py-checkpoint/response.txt
# Expected: writes response metadata to response.txt, no exceptions
deactivate
3. Anti-Patterns & Common Pitfalls
These failure modes appear repeatedly in Python codebases. Each is detectable, preventable, and often rooted in Python's dynamic nature.
Anti-Pattern: Using a List Where a Dict or Set Is Appropriate
A common beginner mistake is iterating over a list to find an element, when a dict keyed by that element is O(1) instead of O(n).
# Bad: O(n) lookup every time
users = [{"id": 1, "name": "alice"}, {"id": 2, "name": "bob"}]
for u in users:
if u["name"] == "alice":
return u
# Good: O(1) lookup by id
users_by_name = {u["name"]: u for u in users}
return users_by_name["alice"]
Detection: search for for ... in loops with if ... == that could be keyed lookups.
Fix: understand the access pattern before choosing a data structure. Unique membership → set. Keyed lookup → dict. Ordered sequence with index → list.
Anti-Pattern: Modifying a List While Iterating Over It
Python's for loop uses the iterator directly; modifying the underlying list invalidates it, causing skipped elements or unexpected behavior.
# Bad: modifies list while iterating — elements are skipped
items = [1, 2, 3, 4, 5]
for i in items:
if i % 2 == 0:
items.remove(i) # may skip the element after the removed one
# Good: iterate over a copy
items = [1, 2, 3, 4, 5]
for i in list(items):
if i % 2 == 0:
items.remove(i)
# Best: use a list comprehension
items = [x for x in items if x % 2 != 0]
Detection: for x in list: ... list.remove(x) or list.pop(index) inside a loop.
Fix: prefer a list comprehension to filter in one pass, or build a new list and replace the original.
Anti-Pattern: Mutable Default Arguments
Default argument values are evaluated once at function definition time, not at each call. A mutable default argument is shared across all calls.
# Bad: the list grows across every call
def add_item(item, items=[]):
items.append(item)
return items
print(add_item("a")) # ['a']
print(add_item("b")) # ['a', 'b'] — "b" is appended to the shared list
# Good: use None and create the list inside the function
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
print(add_item("a")) # ['a']
print(add_item("b")) # ['b']
Detection: def f(..., items=[]) or any mutable object as a default argument.
Fix: use None as the default and initialize inside the function body.
Anti-Pattern: Naive Datetimes in Production
Datetime objects without timezone information are "naive." They do not carry context about what time zone they represent, making arithmetic and serialization ambiguous.
# Bad: naive datetime — what timezone is "local"?
import datetime
now = datetime.datetime.now() # naive
# Good: always use timezone-aware datetimes
from datetime import datetime, timezone
now = datetime.now(timezone.utc) # always UTC, always explicit
Detection: datetime.now() without an argument or datetime.now(timezone.utc) not used in date handling code.
Fix: configure all date sources to emit UTC-aware datetimes. Use zoneinfo.ZoneInfo for conversions. Set a policy: no naive datetimes in server code.
Anti-Pattern: Using Bare except: Without Specifying Exceptions
A bare except: catches KeyboardInterrupt, SystemExit, and every other BaseException subclass, making graceful shutdown impossible.
# Bad: catches everything including KeyboardInterrupt
try:
risky_operation()
except:
log_error()
# Good: catch only the exceptions you can handle
try:
risky_operation()
except (ValueError, IOError) as e:
log_error(e)
raise # re-raise if you cannot fully handle it
Detection: except: without a type, or catching Exception when a specific type is appropriate.
Fix: catch the narrowest possible exception type. Use except Exception only when you genuinely want to handle all non-system-exit errors.
4. Independent Challenge
Build a small log-file analysis CLI without step-by-step guidance.
Produce a single Python script (targeting Python 3.10+) that:
- Accepts a path to a log file as a positional argument and an optional
--levelfilter (DEBUG,INFO,WARNING,ERROR,CRITICAL). - Parses each line assuming a format like
[2026-09-15T10:30:00] [INFO] Message here— a timestamp in brackets, a level in brackets, and the rest as the message. - Aggregates and prints:
- Total line count and count per level.
- The most common message prefix (e.g., the first 50 characters before a colon or the first word).
- A deduplicated list of all unique messages at or above the specified level.
- Writes the deduplicated messages to a file at the same directory as the input with
_filtered.txtappended.
Constraints:
- Must use
argparsefor argument parsing. - Must use
pathlibfor all path operations. - Must use
datetimewith timezone-aware parsing. - No external dependencies (stdlib only).
- Must handle malformed lines gracefully (skip them, count them, report the count).
- The script must run successfully against its own output log file.
Deliverable: a single .py file. Verify by feeding it a generated log file. No solution is provided.
5. Consolidation & Key Invariants
- Indentation is syntax, not style. Mix tabs and spaces causes
IndentationError. Use 4 spaces everywhere. Configure your editor once. - Mutable default arguments persist across calls. Use
Noneand initialize inside the function body for any mutable default. - Choose data structures by access pattern. List/tuple for position, dict for key lookup, set for membership. A list used as a dict is a performance bug.
- Never use naive datetimes in server code. Always use
datetime.now(timezone.utc)or a timezone-aware parser. - Catch narrow exception types. Bare
except:or over-broadexcept Exceptionmasks bugs and prevents graceful shutdown. - Virtual environments are not optional for multi-project work. Use
venvand pin dependencies.pip freezeis the minimum;pip-compilewithpyproject.tomlis the modern standard. - Type hints are documentation that tooling can verify. Add them to function signatures even if you skip them for local variables — they pay off at the boundaries between modules.
6. Next Steps
With Python fundamentals in place, continue your learning path:
- Runtime — Explore how Python runs: CPython internals, PyPy, virtual machines, the Global Interpreter Lock (GIL), and performance considerations.
- Backend Development — Build real services with FastAPI, connect to databases, implement authentication, and deploy to production.
- AI/ML — Apply Python to machine learning with PyTorch, pandas, and LangChain for AI-powered applications.
- Further Reading: Read the official Python tutorial, the "Pythonic" coding style guide, and explore the standard library reference for modules that apply to your domain.
Change Log
| Date | Change |
|---|---|
| 2026-09-15 | Full hydration: expanded all sections with substantive content, added code examples for every concept, added Next Steps and Change Log |
| 2026-08-29 | Initial stub created, section headers defined |
| 2026-09-15 | Migrated to canonical 6-section template (Architectural Overview, Deep Dive with Guided Checkpoint, Anti-Patterns, Independent Challenge, Consolidation, Next Steps). Preserved all prior content, prerequisites, and change log entries with dates. |