Page Contents
- From Version 1 to Version 3 — Why It Evolved
- Template v3 Source Code
- Key Architectural Improvements Explained
- Running with the
aocCLI andMakefile - Daily Workflow
From Version 1 to Version 3 — Why It Evolved
Over several years of Advent of Code, my starter template went through three generations:
- Template v1 (2021): Plain
logging.basicConfig,# INPUT_FILE = ...comment toggling between sample and real data, and manualtime.perf_counter()calls around a single monolithicmain(). - Template v2 (2023): Extracted shared path, HTTP download, and geometry helpers into
aoc_common, addingLocations, automaticwrite_puzzle_input_file(...), andcolorama-coloured logging (ColouredFormatter) — though it still executed downloads eagerly at import time and relied on commenting lines inmain()to switch inputs. - Template v3 (2026 — Current): Separates pure puzzle logic (
parse_input,part1,part2) from I/O and CLI orchestration viaac.run_puzzle(), replaces comment-toggling with declarativeSampleInputverification, swapscoloramaforRich, and integrates directly withuv run aoc.
| Capability | Template v1 (2021) | Template v2 (2023) | Template v3 (2026) | Why Version 3 Is Better |
|---|---|---|---|---|
| Input Retrieval | Manual copy-paste | Import-time write_puzzle_input_file() |
Lazy download inside run_puzzle() + uv run aoc fetch |
Importing a script in tests no longer triggers HTTP calls or fails without .env. |
| Sample vs Real Data | Comment/uncomment INPUT_FILE |
Comment/uncomment locations.sample_input_file |
Declarative PART1_SAMPLES / PART2_SAMPLES + -s / --no-samples |
Zero accidental edits before running real input; sample mismatches fail fast before touching input.txt. |
| Code Structure | Monolithic main() |
Monolithic main() |
Pure parse_input(raw), part1(data), part2(data) |
Keeps Part 1 intact while developing Part 2 and passes fresh parsed copies to each part so mutations cannot leak. |
| Logging & Output | Plain logging.basicConfig |
colorama ColouredFormatter |
RichHandler via ac.setup_logging(stderr=...) + Rich summary table |
Coloured console tracebacks, automatic DEBUG during sample checks, and clean --json stdout streams. |
| Timing & Profiling | Manual t2 - t1 |
Manual t2 - t1 |
Automatic per-part perf_counter via run_puzzle() (format_duration) |
Separate Part 1 and Part 2 timings formatted adaptively in µs, ms, or s. |
| Automation & Scaffolding | Copy files by hand | ./scripts/create_year.sh |
uv run aoc new-year (12-day default) & aoc run / aoc list |
Works uniformly across standalone scripts, agents (--json), and Colab/Jupyter notebooks (check_samples). |
Template v3 Source Code
This is the live starter template stored at src/template_folder/template.py:
"""Author: Darren
Date: 01/12/2026
Solving https://adventofcode.com/2026/day/1
Template v3 standalone solution module powered by :func:`aoc_common.run_puzzle`.
How This Module Works:
1. Paste the example input into ``input/sample_input_part_1.txt`` and update
``PART1_SAMPLES`` (replacing ``None`` with the example's expected answer).
2. Implement ``parse_input(raw)`` — called fresh before each sample and before each
real part so mutating parsed data in Part 1 cannot affect Part 2.
3. Implement ``part1(data)`` and ``part2(data)``. Returning ``None`` marks a part as
not started so it skips cleanly without failing.
4. Run via ``make run <day>``, ``uv run --no-sync aoc run <year> <day> [flags]``,
or directly with ``uv run --no-sync python <script.py> [flags]``:
- ``-s`` / ``--sample-only``: validate sample assertions only (uses ``DEBUG`` logs)
- ``-p {1,2,all}``: run Part 1, Part 2, or both (defaults to ``all``)
- ``--no-samples``: skip sample validation before running on ``input.txt``
- ``-d`` / ``--debug``: keep ``DEBUG`` logging active on real ``input.txt``
- ``--json``: emit machine-readable JSON on stdout and Rich logs on stderr
Real puzzle input (``input/input.txt``) is downloaded lazily on demand when running
against real data and ``AOC_SESSION_COOKIE`` is configured.
Part 1:
Part 2:
"""
from __future__ import annotations
import logging
from collections.abc import Sequence
import aoc_common.aoc_commons as ac # General AoC utils
# Set these to the current puzzle (updated automatically by scripts/create_year.sh and `aoc new-year`)
YEAR: int = 2026
DAY: int = 1
locations = ac.get_aoc_locations(YEAR, DAY, __file__)
logger = logging.getLogger(locations.script_name)
# Sample checks: list of (sample_source, expected_answer) pairs verified before real input.
# - sample_source: Path to a file in input/ (e.g. locations.input_dir / "sample_input_part_1.txt")
# or a raw inline string (e.g. "1abc2\npqr3stu8vwx").
# - expected_answer: expected result for that sample (e.g. 142 or "xyz").
# Leave as None while scaffolding — entries with None are skipped rather
# than failing. Replace None with the puzzle's example answer once known.
PART1_SAMPLES: list[ac.SampleInput] = [
(locations.input_dir / "sample_input_part_1.txt", None), # e.g. (..., 142) or ("inline\nsample", 142)
]
PART2_SAMPLES: list[ac.SampleInput] = [
(locations.input_dir / "sample_input_part_1.txt", None), # or sample_input_part_2.txt if Part 2 differs
]
def parse_input(raw: str) -> list[str]:
"""Parse raw puzzle input into a fresh structure per part."""
return raw.splitlines()
def part1(data: list[str]) -> int | str | None:
"""Solve Part 1 and return the answer (or ``None`` while unimplemented)."""
return None
def part2(data: list[str]) -> int | str | None:
"""Solve Part 2 and return the answer (or ``None`` while unimplemented)."""
return None
def main(argv: Sequence[str] | None = None) -> int:
"""Run sample validation and puzzle parts via the shared Rich runner."""
return ac.run_puzzle(
year=YEAR,
day=DAY,
part1=part1,
part2=part2,
parse_input=parse_input,
part1_samples=PART1_SAMPLES,
part2_samples=PART2_SAMPLES,
locations=locations,
logger=logger,
argv=argv,
)
if __name__ == "__main__":
raise SystemExit(main())
Key Architectural Improvements Explained
1. Import-Safe Modules (get_aoc_locations Without Import-Time I/O)
In Version 2, calling write_puzzle_input_file() at module scope meant merely importing a solution from a test runner or inspection script tried to hit adventofcode.com or read .env. Version 3 computes paths only via ac.get_aoc_locations(YEAR, DAY, __file__), leaving .env loading, git-crypt lock checks, and HTTP caching inside ac.run_puzzle().
2. Pure parse_input and Isolated part1 / part2
Separating parsing from solving ensures that:
- Developing Part 2 cannot accidentally break Part 1’s input representation.
run_puzzle()callsparse_input(raw)freshly for Part 1 and again for Part 2, so in-place mutations (e.g. modifying aGridor popping from alist) in Part 1 never corrupt Part 2.- Unimplemented parts return
None, whichcheck_samples()andrun_puzzle()cleanly skip without failing CI or scaffolds.
3. Declarative SampleInput Pairs
SampleInput is a type alias for tuple[Path | str, Any] — meaning a sample entry can reference either a file on disk (locations.input_dir / "sample_input_part_1.txt") or an inline multi-line string directly in Python. Setting the expected value to None skips assertion while scaffolding; once you read the puzzle example, replacing None with the expected answer turns every subsequent run into an automatic regression test:
PART1_SAMPLES: list[ac.SampleInput] = [
(locations.input_dir / "sample_input_part_1.txt", 42),
("small-inline-edge-case\n1 2 3\n", 6),
]
4. Built-In Runner Flags, Rich Tables, and --json Mode
Passing argv into ac.run_puzzle() equips every day script with consistent CLI flags:
-s/--sample-only: Run sample checks with automaticDEBUGlogging without requiring or downloadinginput.txt.-p {1,2,all}/--part {1,2,all}: Focus on Part 1, Part 2, or both.--no-samples: Skip sample checks when iterating on real input performance.-d/--debug: KeepDEBUGlogging enabled when running against realinput.txt(by default, real runs drop back toINFOso verbose sample traces don’t flood your terminal).--json: Route Rich logs tostderrand emit a clean JSON execution summary tostdoutfor scripts and AI coding agents.
Running with the aoc CLI and Makefile
Rather than invoking deeply nested paths by hand, pyproject.toml exposes a repository-wide aoc entry point via uv run aoc (with matching shortcuts in Makefile):
uv run aoc new-year 2026 # Scaffold AoC_2026/d01..d12 from Template v3 (default: 12 days for 2025+)
uv run aoc fetch 2026d01 --write-sample # Fetch puzzle description in memory and populate sample_input_part_1.txt
uv run aoc run 2026d01 -s -d # Run sample checks with DEBUG logging (or: make run 2026d01 ARGS="-s -d")
uv run aoc run 2026 1 -p 2 # Run Part 2 on real input.txt
uv run aoc list 2026 # Rich progress table with clickable OSC 8 links to adventofcode.com
Notice that Advent of Code moved from a 25-day event (2015–2024) to a 12-day event from 2025 onwards, so uv run aoc new-year defaults to --days 12 unless you pass --days 25 for a legacy year.
Running a scaffolded script in sample-only mode (uv run aoc run 2018d07 -s) emits clean Rich logs followed by a colour-coded summary table:
INFO 22:31:59.334 Part 1 returned None (not implemented yet); skipping sample check
DEBUG 22:31:59.336 Part 2 sample 1 skipped (expected answer is None)
Advent of Code 2018 Day 07
| Part | Status | Samples | Answer | Time |
|---|---|---|---|---|
| Part 1 | ok |
0 |
- |
- |
| Part 2 | ok |
0 |
- |
- |
Daily Workflow
- Fetch description & sample:
uv run aoc fetch 2026d01 --write-sample - Wire expected sample value into
PART1_SAMPLESand implementparse_input()+part1(). - Iterate offline-first:
uv run aoc run 2026d01 -s -duntil Part 1 passes every sample. - Solve real input:
uv run aoc run 2026d01 -p 1, submit manually on adventofcode.com, then repeat for Part 2 (-p 2). - Verify quality:
make check(codespell,ruff,pyright, andpytest src/tests).