6. Paths, dates and resources

So far we have cheated a little. We treated file paths as plain strings and the buoys' timestamps as str that we never interpreted. It works in an example, but in real code it costs you dearly. This lesson fixes three habits with three standard-library tools, without installing anything: pathlib for paths, datetime for time and context managers for the resources you have to open and close.

Paths with pathlib

Concatenating paths by hand (data_dir + "/" + name) is fragile: it changes between Windows and Linux, an extra slash slips in and you end up with data//buoy.csv. The standard library ships pathlib, which treats paths as objects with methods.

from pathlib import Path

data_dir = Path("data/buoys")

# Build paths with the / operator, no fighting with slashes
today_log = data_dir / "2026-01-15.csv"

# Ask the path questions
today_log.exists()  # does it exist?
today_log.name  # "2026-01-15.csv"
today_log.stem  # "2026-01-15"
today_log.suffix  # ".csv"
today_log.parent  # Path("data/buoys")

Walking a folder stops being a problem. glob filters by pattern:

# All the CSV logs in the buoy folder
for log_file in data_dir.glob("*.csv"):
    print(log_file.name)

And reading or writing a small file is a single line:

content = today_log.read_text(encoding="utf-8")

The habit you take away: pass Path through your functions, not str. Your editor and the type checker (we'll see it in the last lesson) will warn you if you get it wrong, and the code reads better. The day an old library asks you for the string, str(path) gets it out.

Time with datetime

A timestamp stored as "2026-01-15T10:00" is just text: you can't subtract two, or tell which is earlier, or add an hour to it. That is what datetime is for.

from datetime import datetime, timedelta, timezone

# From ISO 8601 text to a datetime object
reading_time = datetime.fromisoformat("2026-01-15T10:00:00+00:00")

# Real time arithmetic
one_hour_later = reading_time + timedelta(hours=1)
gap = one_hour_later - reading_time  # a timedelta
gap.total_seconds()  # 3600.0

fromisoformat reads the ISO 8601 format, the standard almost all sensors and APIs use. timedelta represents a duration, and subtracting two datetime values gives you one.

Naive versus aware. A datetime without a time zone (naive) is a time bomb: you don't know whether that ten in the morning is in London or in the Canaries. Always work with aware dates, anchored to UTC, and convert to local time only for display. datetime.now(tz=timezone.utc) gives you the current moment with no ambiguity. In marine engineering, with buoys spread across several time zones, mixing local times is the same expensive mistake as confusing meters with feet.

Remember how in the buoy reader we stored the timestamp as a str? Now you know why it was a smell. In a real project, parse_reading would convert that field into a datetime at the border, and the rest of the program would work with real time, not with text.

Resources that close themselves: context managers

You have already written with open(path) as file: many times. That with is a context manager, and now it is time to understand what it does and why it matters.

A resource (a file, a connection to a sensor, a network socket) has to be opened and, no matter what, closed. If you forget, or if an exception fires in the middle, the resource is left hanging. The with guarantees the close even if the block blows up:

# The file closes itself when leaving the block, even if there is an error inside
with open("buoy.csv", encoding="utf-8") as file:
    process(file)

The best part is that you can write your own. The cleanest way uses a generator, exactly what you learned in the functional lesson: what goes before the yield is the opening, and what goes in the finally, the closing.

from contextlib import contextmanager


@contextmanager
def open_sensor(port: str):
    """Open a connection to the sensor and guarantee that it closes."""
    connection = connect(port)  # setup, before the yield
    try:
        yield connection  # this is what the 'as' receives
    finally:
        connection.close()  # cleanup, no matter what


with open_sensor("/dev/ttyUSB0") as sensor:
    reading = sensor.read()
# here the connection is already closed

The try/finally is the key: the finally always runs, error or no error. If you prefer the object-oriented approach from the previous lesson, a context manager can also be written as a class with the __enter__ and __exit__ methods; the @contextmanager decorator is just the short version.

This connects with the idea that runs through the course: isolating what touches the outside world. A context manager puts the opening and closing of a resource in one single place, so the rest of your code doesn't have to remember.

Guided example: reading a folder of logs by time window

Let's bring the three tools together: walk a folder of CSVs with pathlib, interpret each line's time with datetime and read each file with a with. The function delivers only the readings within a time window. In src/metocean_tools/archive.py:

from datetime import datetime
from pathlib import Path


def readings_between(folder: Path, start: datetime, end: datetime):
    """Deliver the readings from all the folder's CSVs within a window.

    Args:
        folder: Folder with the buoy logs, one CSV per day.
        start: Start of the time window, inclusive.
        end: End of the time window, inclusive.

    Yields:
        Tuples (datetime, float): moment and wave height, in meters.
    """
    for log_file in sorted(folder.glob("*.csv")):
        with log_file.open(encoding="utf-8") as file:
            next(file)  # skip the header
            for line in file:
                raw_time, raw_wave, _ = line.strip().split(",")
                moment = datetime.fromisoformat(raw_time)
                if start <= moment <= end:
                    yield moment, float(raw_wave)

Notice how everything fits: pathlib finds and opens the files, the with guarantees they close, datetime turns the text into comparable time and the yield keeps memory consumption constant even if the folder has hundreds of files, exactly like in the functional lesson.

This work is under a Attribution-NonCommercial-NoDerivatives 4.0 International license.

Building SPAs with Django and HTML Over the Wire: Learn to build real-time single page applications with Python

Building SPAs with Django and HTML Over the Wire: Learn to build real-time single page applications with Python

The HTML over WebSockets approach simplifies single-page application (SPA) development and lets you bypass learning a JavaScript rendering framework such as React, Vue, or Angular, moving the logic to Python. This web application development book provides you with all the Django tools you need to simplify your developments with real-time results.

Buy the book

Help me keep writing

Every coffee gives me a push toward the next article.

Comments

There are no comments yet.