Spoiler alert
This is a full solution for Advent of Code 2025, day 6 puzzles with explanations.
If you want to try to solve it yourself first, head over to https://adventofcode.com/2025/day/6 and give it a go and then come back and compare notes.
You can find the full code at https://github.com/Hamatti/adventofcode-2025/blob/main/src/day_6.py
I was expecting the first Saturday puzzle to up the complexity and challenge a bit and it sure did. But in a really fun way, I had a blast solving today’s puzzle which is all about figuring out how to parse a weird input format.
Read input
A kid is doing her math homework and needs help.
The equations are provided in a column-based format:
123 328 51 64
45 64 387 23
6 98 215 314
* + * +
First, I need a map function to read this in with my system. Here’s where using such system becomes a tradeoff: it would be better to directly read the data into the right format to avoid extra processing but the benefit is that most of the days, it removes the need to worry about parsing data.
def mapper(line: str) -> List[int]|List[str]:
"""Reads either a line of numbers or a line of +/* characters"""
parts = line.split()
try:
return [int(elem) for elem in parts]
except ValueError:
return partsPython is often written in what is called duck typing style: instead of checking if something is a number and deciding how to move forward, we try to perform action that can be run on numbers and if we get an error or exception, we catch that and do something else.
Here, we try to convert each element on a line into an integer and if that fails (which will be the case with the last line), we return the elements without conversion.
Part 1
The first challenge is to convert the input into a column-based equations and calculate either the product or the sum based on the final row’s operation.
I use a dataclass to model our calculations. I actually wrote it to simplify part 2 but then refactored part 1 to use it as well since it makes the code read nicer. This CephalopodCalculation has two fields: an operation which is either * or + and numbers which is a list of integers.
It also has one method, calculate, which returns the outcome of that calculation. In it, we’re using pattern matching. To pattern match, we use match keyword to tell the computer what we are looking at: in this case the operation. We then provide multiple case clauses that provide different patterns we match against.
At the end, we have a default case _ which is needed in case none of the other patterns match. With this data, it should never run into that case but it’s also a good way to see if there are mistakes either in the data or how we parsed it.
from dataclasses import dataclass
@dataclass
class CephalopodCalculation:
operation: Literal['*', '+']
numbers: List[int]
def calculate(self) -> int:
match self.operation:
case '*':
return prod(self.numbers)
case '+':
return sum(self.numbers)
case _:
raise NotImplementedError(f'Unknown opeator {self.operation}')First we read the data in line-by-line.
We then need to turn it “90 degrees”: make our rows into columns and vice versa. This is called transposing a matrix and there’s a really handy trick in Python for it: we use zip and give the original list of list to it with an asterisk before it. The asterisk tells Python to unpack the variable and when used with a zip, it transposes the list of lists.
def part_1() -> int:
"""Calculate sums and products of column based equations"""
data = read_input(6, mapper)
calculations = zip(*data)
total = 0
for calc in calculations:
total += CephalopodCalculation(calc[-1], calc[:-1]).calculate()
return totalNow that we’ve transposed the calculations into column-based ones, we can create our dataclass, perform the calculation and count the total value.
Part 2
For the second part, we got some extra spice. This time, the complexity of the puzzle comes from the fact that we need to parse the data in even more wonky ways. This time, the alignment of the numbers matters as we need to do two column-based parsings: we still care about the blocks of calculations like in the first part but now also the numbers are actually presented in columns as well.
Looking back at the example’s first block
123
45
6
*
now the actual numbers are 1 (first column), 24 (second column) and 256 (third column).
I decided to solve this by parsing the input twice.
First, I used the same mapper and transposed the matrix as in part one. But then I reparsed it based on what I had learned from the data.
def reparse(data: List) -> List[List[str]]:
# Read input file into list of lines
with open(INPUT_FILE, 'r') as inputfile:
raw_data = inputfile.readlines()
correct_calculations = []
# Current block's start offset
cursor = 0
for block in data:
# Calculate how wide a block is by finding the longest
# string representation of numbers in block
block_length = max(len(str(num)) for num in block[:-1])
# Read block_length characters with spaces preserved
calculation = []
for line in raw_data:
item = line[cursor:cursor+block_length]
calculation.append(item)
correct_calculations.append(calculation)
# Move cursor to the start of next block
# +1 is required to take into account
# the space between blocks
cursor = cursor + block_length + 1
return correct_calculationsThe key here is that for each block of numbers, I find the longest of them and always read that many characters from each line. I then move the cursor to the start of the next block and do the same.
Once we have our correctly parsed blocks with spaces intact, we need to turn them into correct numbers.
def convert_to_cephalopod_math(blocks: List[List[str]]) -> List[CephalopodCalculation]:
"""Convert column-based data blocks into CephalopodCalculations"""
calculations = []
for block in blocks:
operation = block[-1].strip()
numbers = defaultdict(str)
for entry in block[:-1]:
for col, value in enumerate(entry):
if value == ' ':
continue
numbers[col] += value
calculations.append(CephalopodCalculation(operation, [int(num) for num in numbers.values()]))
return calculationsFor each block, we create a numbers dictionary using defaultdict to construct new numbers based on which column they appear. defaultdict is a subclass of dictionary that lets us define what datatype it defaults to. Using str as the default, if we try to access a key that doesn’t exist, instead of erroring out, it will return an empty string. This enables us to do numbers[col] += value without having to worry if we have previously ran into numbers in that column.
If the value is a space, we ignore it. We can then create the actual CephalopodCalculation by looking at all the .values() of the dictionary and turning them into integers.
Once we have these in place, we can run the main code
def part_2() -> int:
raw_input = read_input(6, mapper)
data = zip(*raw_input)
blocks = reparse(data)
calculations = convert_to_cephalopod_math(blocks)
total = 0
for calc in calculations:
total += calc.calculate()
return totalI was really focusing here on the readability of the flow. I split things into multiple smaller functions and did some extra work in reading and parsing the data twice to make the code cleaner to read at the expense of running extra CPU cycles.
When writing code, we always have to balance between computer performance and code readability. Sometimes we need the code to be extra performant and we make a tradeoff of making it less easy to read and modify. Often however we are not that pressed with performance and in those cases, computer time is cheaper than developer time so the tradeoff becomes the opposite.
I really enjoyed today’s puzzle. It wasn’t a “this is hard because it requires difficult math” but it was challenging in a “let me figure out how to parse hard things” way. Solving today’s puzzle took me 2x time compared to the previous days and it was a great way to kick off Saturday.
We’re halfway done with Advent of Code! 12/24 stars collected. Wahoo! as our favourite star collecting plumber would say.
Addendum A: alternative with single pass parsing
My less than beautiful double-parsing kept bothering me throughout the day and my discussion with Marcos made me realise that the operation code + or * was always left-aligned and that could be used as a guideline for when to stop adding numbers.
I renamed my dataclass from CephalopodCalculation to Equation as I realised there was nothing specific on an individual equation level that made the math different, only on the parsing level.
from dataclasses import dataclass
@dataclass
class Equation:
operation: Literal['*', '+']
numbers: List[int]
def calculate(self) -> int:
match self.operation:
case '*':
return prod(self.numbers)
case '+':
return sum(self.numbers)
case _:
raise NotImplementedError(f'Unknown opeator {self.operation}')I then added a helper function that takes a cursor position and a list of lists matrix of the input (without any transposing) and returns a both the operation (or None) and the number.
def convert_column(cursor: int, data) -> Tuple[str|None, int]:
number = ''
op = None
for line in data:
char = line[cursor]
if char == ' ': continue
if char in ('*', '+'):
op = char
break
number += char
return op, int(number)This time, I’m not creating a namedtuple for this return value because it’s never accessed as a tuple as I unpack it immediately after the function call:
def read_equations() -> List[Equation]:
equations = []
with open(INPUT_FILE, 'r') as inputfile:
data = inputfile.read().split('\n')
cursor = len(data[0]) - 1
numbers = []
while cursor >= 0:
op, number = convert_column(cursor, data)
numbers.append(number)
cursor -= 1
if op:
equations.append(Equation(op, numbers))
numbers = []
cursor -= 1
continue
return equationsI read the data and split it by lines. I start the cursor at the far end.
For each cursor position, I get the operation and number, add the number to current equation’s numbers list and move one step to left. If the column included an operation, I finish the Equation, clear up temporary variables and move an extra step to left (as there’s always an empty space between equations) and continue to the next cursor position.
def part_2() -> int:
equations = read_equations()
return sum(eq.calculate() for eq in equations)Part 2 then becomes simply reading the data and summing up all equation calculations.
I think this became much better. You can find the full code in GitHub.
Get in touch
If you want to comment on these solutions, I share them in Mastodon where you can look for the post for the right date or you can start a discussion over email with juhamattisantala@gmail.com.
Follow via RSS
I have created a custom RSS feed for these solutions so you can follow along and not miss any of them!