diff --git a/2023/python/Day03.py b/2023/python/Day03.py
new file mode 100644
index 0000000000000000000000000000000000000000..8f04c08eb7f972c258d66686be120e41ce92dc1c
--- /dev/null
+++ b/2023/python/Day03.py
@@ -0,0 +1,129 @@
+from typing import List, Optional
+
+from ImportData import import_data
+
+day: int = 3
+
+sample_data: List[str] = '''
+467..114..
+...*......
+..35..633.
+......#...
+617*......
+.....+.58.
+..592.....
+......755.
+...$.*....
+.664.598..
+'''.split('\n')[1:-1]
+
+data_structure: type = List[str]
+
+
+def parse_data(data: List[str]) -> data_structure:
+    # add some padding so that we won't have to look for borders later
+    padded_data: List[str] = []
+    for string in data:
+        padded_data.append(f' {string} ')
+    length = len(padded_data[0])
+    padded_data.append('.' * length)
+    padded_data.insert(0, '.' * length)
+    return padded_data
+
+
+def is_part_number(data: data_structure, row: int, first_column: int, last_column: int) -> bool:
+    symbols = frozenset({'!', '@', '#', '$', '%', '^', '&', '*', '-', '+', '=', '/', '?'})
+    found_symbol: bool = False
+    for symbol in symbols:
+        if symbol in data[row - 1][first_column - 1:last_column + 1] \
+                or symbol in data[row][first_column - 1:last_column + 1] \
+                or symbol in data[row + 1][first_column - 1:last_column + 1]:
+            found_symbol = True
+    return found_symbol
+
+
+def part1(data: data_structure) -> int:
+    number: Optional[int] = None
+    first_column: int = 0
+    last_column: int
+    part_numbers: List[int] = []
+    for row, string in enumerate(data):
+        for column, character in enumerate(string):
+            if character.isdigit():
+                if number is None:
+                    number = int(character)
+                    first_column = column
+                else:
+                    number = 10 * number + int(character)
+            else:
+                if number is not None:
+                    last_column = column
+                    if is_part_number(data, row, first_column, last_column):
+                        part_numbers.append(number)
+                    number = None
+    return sum(part_numbers)
+
+
+class Gear:
+    gears: List["Gear"] = []
+
+    def __init__(self, row: int, column: int):
+        self.row = row
+        self.column = column
+        self.numbers: List[int] = []
+
+    @classmethod
+    def add_number_to_gear(cls, number: int, row: int, column: int):
+        candidates = [gear for gear in Gear.gears if gear.row == row and gear.column == column]
+        gear: Gear
+        if candidates:
+            gear = candidates[0]
+        else:
+            gear = Gear(row, column)
+            Gear.gears.append(gear)
+        gear.numbers.append(number)
+
+
+def look_for_gear(number: int, data: data_structure, row: int, first_column: int, last_column: int):
+    try:
+        column = data[row - 1].index('*', first_column - 1, last_column + 1)
+        Gear.add_number_to_gear(number, row - 1, column)
+    except ValueError:
+        pass
+    try:
+        column = data[row].index('*', first_column - 1, last_column + 1)
+        Gear.add_number_to_gear(number, row, column)
+    except ValueError:
+        pass
+    try:
+        column = data[row + 1].index('*', first_column - 1, last_column + 1)
+        Gear.add_number_to_gear(number, row + 1, column)
+    except ValueError:
+        pass
+
+
+def part2(data: data_structure) -> int:
+    number: Optional[int] = None
+    first_column: int = 0
+    last_column: int
+    for row, string in enumerate(data):
+        for column, character in enumerate(string):
+            if character.isdigit():
+                if number is None:
+                    number = int(character)
+                    first_column = column
+                else:
+                    number = 10 * number + int(character)
+            else:
+                if number is not None:
+                    last_column = column
+                    look_for_gear(number, data, row, first_column, last_column)
+                    number = None
+    return sum([gear.numbers[0] * gear.numbers[1] for gear in Gear.gears if len(gear.numbers) == 2])
+
+
+if __name__ == '__main__':
+    production_ready = True
+    raw_data = import_data(day) if production_ready else sample_data
+    print(part1(parse_data(raw_data)))
+    print(part2(parse_data(raw_data)))