These are my Advent of Code solutions starting in 2023 using Python.
This repo has two main commands: start
and advent
.
./start [-h] [--year YEAR] [day]
Scaffold files to start a new Advent of Code solution
positional arguments:
day
(optional): Which puzzle day to start, between[1,25]
. Defaults to the next day without a folder (matchingday_NN
) in the specified year.
optional arguments:
-h, --help
(optional): show this help message and exit--year YEAR
(optional): Puzzle year. Defaults to current year if December has begun, otherwise previous year.
./start
./start 2
./start 3 --year 2019
./advent [--year year] [--test-data] [--debug] [--profile] [--slow] [--time] [day]
Run a specific day of Advent of Code
informational flags
-h, --help
: Show this help message and exit--version
: Print version info and exit
positional arguments:
day
(optional): Which puzzle day to start, between[1,25]
. Defaults to the latest day with a folder (matchingday_NN
) in the specified year.
optional flags:
--year YEAR
: puzzle year. Defaults to current year if December has begun, otherwise previous year-t, --test-data
: read puzzle input frominput.test.txt
instead ofinput.txt
. Ignores@answer
decorators (see saving answers)--debug
: prints normally-hidden debugging statements (written withself.debug(...)
). See debugging--profile
: run solution through a performance profiler--slow
: specify that long-running solutions (or those requiring manual input) should be run. They're skipped otherwise--time
: print information about how long solutions took to run. More useful than timing at a shell level, since this only starts the timer once all imports have happened and anyadvent
-related code is done.
./advent
./advent 2
./advent 5 --year 2019
./advent 7 --test-data
./advent 9 -t --debug
solutions/
├── ...
└── 2020/
├── day_01/
│ ├── solution.py
│ ├── input.txt
│ ├── input.test.txt
│ └── README.md
├── day_02/
│ ├── solution.py
│ ├── ...
└── ...
- each year has a folder (
YYYY
) - each day in that year (will eventually) have a folder (
day_NN
)
Each day_NN
folder has the following files:
solution.py
, which has aclass Solution
../advent
expects both that filename and that class name exactly, so you shouldn't change them. See Writing Solutions for how the file is structuredinput.txt
holds your individualized input from the AoC site. Make sure not to share it publicly!input.test.txt
holds the example input from the prompt. It's read when the--test-input
flag is used (see below). It also won't throw errors if the result doesn't match the answer. You can also do all your work ininput.txt
, but it's marginally less convenientREADME.md
is a convenient place to take notes or explain your solution
A helpful base class on which to build your AoC solutions. It's got 2 required
properties (which should be pre-filled if you use ./start
): _year
and
_day
, corresponding to the puzzle you're solving.
Your puzzle input, the parsed contents of the day's input.txt
, will be
available at self.input
. Learn more in Reading Input.
There's also a convenience method for print-based debugging: self.debug()
.
You can pass it any number of items and they'll get pretty-printed. It only
prints if you use the --debug
flag with ./advent
.
AoC input takes a number of forms, so there are a number of simple modes for
input parsing. Your generated Solution
class should inherit from one of the
following classes, which will parse self.input
for you:
Inherited Class | description | sample for this mode |
---|---|---|
TextSolution |
one solid block of text; the default | abcde |
IntSolution |
one number | 12345 |
StrSplitSolution |
str[] , split by a specified separator (default newline) |
a b c d e |
IntSplitSolution |
int[] , split by a specified separator (default newline) |
1 2 3 4 5 |
# input file is "12345"
from ...base import (
IntSolution,
IntSplitSolution,
StrSplitSolution,
TextSolution,
)
for BaseClass in [TextSolution, IntSolution, StrSplitSolution, IntSplitSolution]:
class Solution(BaseClass):
def show_input(self):
print(f"\n{self.input} (type: {type(self.input)})\n")
Solution().show_input()
# 12345 (type: <class 'str'>)
# 12345 (type: <class 'int'>)
# ['12345'] (type: <class 'list'>)
# [12345] (type: <class 'list'>)
You can also change the separator to change how the SplitSolution
s work:
# input file is "1,2,3,4,5"
from ...base import IntSplitSolution, StrSplitSolution
for BaseClass in [StrSplitSolution, IntSplitSolution]:
class Solution(BaseClass):
separator = ","
def show_input(self):
print(f"\n{self.input} (type: {type(self.input)})\n")
Solution().show_input()
# ['1', '2', '3', '4', '5'] (type: <class 'list'>)
# [1, 2, 3, 4, 5] (type: <class 'list'>)
Each AoC puzzle has two parts, so there are two functions you need to write:
part_1
and part_2
. Each should return an int
, since that's typically the
answer that AoC expects.
Sometimes, it's easier to calculate both parts in a single function (such as if
the answer is asking about two parts of a single computation). In that case,
there's also a solve()
method, which should return a 2-tuple with your
answers (like (5, 7)
). solve
takes precedence if present. Feel free to
delete any unused functions when you're done.
class Solution(TextSolution):
def part_1(self) -> int:
return some_computation()
def part_2(self) -> int:
return some_other_computation()
# or:
def solve(self) -> tuple[int, int]:
part_1 = 0
total = 0
for i in range(10):
result = some_computation()
if i == 0:
part_1 = result
total += result
return result
Once you've solved the puzzle, you can decorate your answer function (solve
or part_N
) with the @answer
decorator. It asserts that the value returned
from the function is whatever you pass to the decorator:
class Solution(TextSolution):
_year = 2022
_day = 5
@answer(123)
def part_1(self) -> int:
return 123
@answer(234)
def part_2(self) -> int:
return 123 # err!
This is helpful for ensuring your answer doesn't change when editing your code after you've solved the puzzle. It's included as a comment in the template. It's ignored when running against test input, so it's easy to verify as you go.
The base class includes a self.debug
method which will pretty-print all
manner of inputs. These only show up when the --debug
flag is used, making it
a convenient way to show debugging info selectively.
I recommend the following tools:
- Ruff, a lightning-fast linter (to help you catch bugs)
- Pyright, a type checker to identify logical errors (also available in VSCode using their Python plugin)
If you have both available, then just lint
will run them both. I've included
a simple ruff
configuration file to help get you started.
If you're running many solutions at once and want to exclude individual parts
of solutions (or entire days), you can mark individual functions with the
@slow
decorator. They'll print a warning, but won't actually run the
solution.