Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Work with files 🔥

Most Advent puzzles hand you a text file and ask you to turn its contents into something useful. Reading that input is often the first step toward solving the puzzle, whether you’re working with numbers, maps, instructions, or something much stranger. Mojo gives you both convenient whole-file operations and lower-level file handles when you need more control.

In this chapter, you’ll follow a temperature log through a complete file workflow. You’ll find and inspect the input file, read and split its contents, create a report, append new data, and clean up when you’re done. Along the way, you’ll work with Path, FileHandle, file modes, and context managers, tools you can reuse throughout Advent of Code and in your own programs.

Mojo inspecting a North Pole temperature monitor. Mojo inspecting a North Pole temperature monitor.

Grab your data

Things are getting a little frosty at the North Pole.

Santa embraced the Internet of Things years ago, and temperature sensors are scattered all over the workshop. Rudolph worries about visibility for the sleigh, but his nephew Chiller records the daily sensor logs.

Download the sample input used for this page:

temp_log.txt

Place it in your work folder, next to where you’ll build your program.

Read the sensor log

Unlike the previous page, your data already exists. That’s how Advent of Code usually works. You download the puzzle input and let your program do the reading. Create temp_log.mojo so you can start reading:

from std.pathlib import Path


def main() raises:
    var log = Path("src/downloads/temp_log.txt")
    print(log.read_text())  # Day 1: -20.5C, Partly Cloudy, etc

Run this to make sure your content matches the file.

Takeaways

  • Path wraps a filesystem path.
  • read_text() opens the file, reads the whole file as a String, then closes it again. If you duplicate the final line here, you get the same data.

Ask the filesystem a few questions before you continue. These are good calls to have on-hand in your Mojo vocabulary:

    print(t"Exists: {log.exists()}")
    print(t"File:   {log.is_file()}")
    print(t"Dir:    {log.is_dir()}")
  • exists(), is_file(), and is_dir() return Bool.
  • Production code knows about race conditions and should be ready to handle failures between checks and operations.

Try this

File operations can fail. That’s why main() declares raises.

Try changing "temp_log.txt" to a filename that doesn’t exist (like “path/to/nowhere”, unless you actually have a file with that path). Then:

  • Remove raises and observe the compile-time error.
  • Restore raises, then catch the runtime error with try/except.
  • Put the filename back when you’re finished.

Split into lines

Most puzzles want you to process the input a line at a time rather than as one long block of text:

    var text = log.read_text()
    var lines = text.splitlines()
    print(t"Got {len(lines)} lines")  # Got 10 lines

Takeaways

  • splitlines() returns a List[StringSpan].
  • Each slice refers to data owned by text, so keep text alive while using the slices.
  • splitlines() handles \n, \r\n, and \r, and doesn’t return an extra empty line for the final newline.

Create a report

The elves want proof that today’s sensor log arrived before anyone starts analyzing it. Add this:

    var report = Path("report.txt")

    report.write_text(
        String(
            "North Pole Temperature Report",
            "=============================",
            "",
            "Input: temp_log.txt",
            "Status: Received",
            "",
            "Waiting for analysis...",
            "",
            sep="\n",
        )
    )

Open report.txt in your editor.

Takeaways

  • write_text() creates the file if needed.
  • If it already exists, the old contents are replaced.
  • Like read_text(), it handles opening and closing the file for you.

Append content

Just as you’re about to leave, Chiller radios in one last temperature.

Rather than replacing the report, append the new reading, which is -52.3. Oof, it’s cold out there.

    with open(report, "a") as f:
        var value = -52.3
        f.write(t"\nLate reading: {Float64(value)} °C")
    print(report.read_text())

Check the report to see the new line.

Takeaways

  • open(path, mode) returns a FileHandle. Modes: "r" read, "w" write (truncates), "rw" read-write, "a" append.
  • FileHandle.write() accepts any Writable value.
  • Inside the block, f.write(value) writes any Writable value.

Context managers

When you call open(), it returns a context manager typed as FileHandle. Context managers are types that implement two methods: __enter__() and __exit__(). Mojo calls these at the start and exit of with statements, helping you add custom code for set-up and tear-down.

FileHandle automatically close the file it’s managing when execution leaves the with block. Managers run __exit__() whether the block ends normally or with errors.

You focus on reading and writing, instead of having to close the file directly.

Takeaways

  • The binding used for as (in this case binding f) should always be a context manager.
  • Context managers use duck typing. Implement the two dunder methods (methods whose names start and end with double underscores), and use the type for with. There’s no context manager trait.

Clean up

You just got notice from Santa’s security point-elf. Apparently, unlike your raw data, the report you generated is proprietary to North Pole Operations. Time to perform your polar data safety protocol:

    from std.os import remove

    try:
        if report.exists():
            remove(report)
    except e:
        print(t"File removal failed: {e}")

    print(t"After cleanup, exists: {report.exists()}")

Takeaways

  • remove() deletes a file and raises if the operation fails.
  • Generated files are good candidates for cleanup. Puzzle inputs usually stick around for future runs.
  • For production, make sure you cover race conditions and other situations where removing the file fails with try/except statements.

Final code

Your complete temp_log.mojo:

from std.pathlib import Path


def main() raises:
    var log = Path("src/downloads/temp_log.txt")
    print(log.read_text())  # Day 1: -20.5C, Partly Cloudy, etc

    print(t"Exists: {log.exists()}")
    print(t"File:   {log.is_file()}")
    print(t"Dir:    {log.is_dir()}")

    var text = log.read_text()
    var lines = text.splitlines()
    print(t"Got {len(lines)} lines")  # Got 10 lines

    var report = Path("report.txt")

    report.write_text(
        String(
            "North Pole Temperature Report",
            "=============================",
            "",
            "Input: temp_log.txt",
            "Status: Received",
            "",
            "Waiting for analysis...",
            "",
            sep="\n",
        )
    )

    with open(report, "a") as f:
        var value = -52.3
        f.write(t"\nLate reading: {Float64(value)} °C")
    print(report.read_text())

    from std.os import remove

    try:
        if report.exists():
            remove(report)
    except e:
        print(t"File removal failed: {e}")

    print(t"After cleanup, exists: {report.exists()}")

Topics covered

Context managers, reading, writing, and deleting files, paths and file checks.