From 965bade9d4bcde1612012c5cadbb122c423aed02 Mon Sep 17 00:00:00 2001 From: jbranchaud Date: Fri, 28 Aug 2026 13:46:28 -0500 Subject: [PATCH] Add Register SQLite Adapter To Serialize Datetimes as a Python TIL --- README.md | 3 +- ...r-sqlite-adapter-to-serialize-datetimes.md | 93 +++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 python/register-sqlite-adapter-to-serialize-datetimes.md diff --git a/README.md b/README.md index 7aa699c..f653640 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ working across different projects via [VisualMode](https://www.visualmode.dev/). For a steady stream of TILs, [sign up for my newsletter](https://visualmode.kit.com/newsletter). -_1874 TILs and counting..._ +_1875 TILs and counting..._ See some of the other learning resources I work on: @@ -1126,6 +1126,7 @@ If you've learned something here, support my efforts writing daily TILs by - [Parse Relative Time To datetime Object](python/parse-relative-time-to-datetime-object.md) - [Publish A Package To A Test Env As A Dry Run](python/publish-a-package-to-a-test-env-as-a-dry-run.md) - [Reclassify Certain Packages As Dev Dependencies](python/reclassify-certain-packages-as-dev-dependencies.md) +- [Register SQLite Adapter To Serialize Datetimes](python/register-sqlite-adapter-to-serialize-datetimes.md) - [Resurface Exceptions Swallowed By Click Under Test](python/resurface-exceptions-swallowed-by-click-under-test.md) - [Select Implementation With Class Registry](python/select-implementation-with-class-registry.md) - [Set Up Pyright Type Checking In GitHub](python/set-up-pyright-type-checking-in-github.md) diff --git a/python/register-sqlite-adapter-to-serialize-datetimes.md b/python/register-sqlite-adapter-to-serialize-datetimes.md new file mode 100644 index 0000000..fa33ffb --- /dev/null +++ b/python/register-sqlite-adapter-to-serialize-datetimes.md @@ -0,0 +1,93 @@ +# Register SQLite Adapter To Serialize Datetimes + +SQLite doesn't have a datetime or timestamp data type, so that information has +to be stored as `text` or a unix epoch `int`. That means when using Python's +`sqlite3` module to perform writes I need to tell it what shape to write a +datetime value. + +I could manually convert `datetime` values everywhere they are involved in +_write_ operations. + +```python +# Prepare `sessions` insert payload +session_data = { + "active": 1 if active else 0, + "project_id": project_id, + "start_time": datetime.isoformat(session.start_time), + "end_time": None, +} + +if session.end_time: + session_data["end_time"] = datetime.isoformat(session.end_time) + +# Insert the new active session +cursor = self.conn.execute( + """ + insert into sessions (active, project_id, start_time, end_time) + values (:active, :project_id, :start_time, :end_time) + returning id; +""", + session_data, +) +``` + +I've used `datetime.isoformat` above which formats `datetime` objects like so: + +```python +>>> datetime.now().isoformat() +'2026-08-28T11:52:04.709907' +``` + +I'd like to make two improvements. + +1. I want these `datetime` values to be formatted instead like `2026-08-28T18:15:27.213Z`. +2. I want `datetime` values to be serialized automatically in the specific shape + without having to manually convert them everywhere. + +I can achieve both of these things by [registering an adapter with `sqlite3`](https://docs.python.org/3/library/sqlite3.html#how-to-register-adapter-callables) +that handles the serialize of `datetime` objects. + +First, I define a function that can perform the `datetime` to `str` conversion. +I decided to put this in `db.py` where I have some other database-specific +functions. + +```python +from datetime import datetime, timezone + +def to_db(dt: datetime) -> str: + if dt.tzinfo is None or dt.utcoffset() is None: + raise ValueError(f"Unable to store naive datetime: {dt!r}") + dt = dt.astimezone(timezone.utc) + return f"{dt:%Y-%m-%dT%H:%M:%S}.{dt.microsecond // 1000:03d}Z" +``` + +Then I register the adapter before creating the connection that gets used for +database interactions. + +```python +import sqlite3 +from datetime import datetime +from pathlib import Path +from sqlite3 import Connection + +def initialize_conn(db_file: Path) -> Connection: + # register adapters + sqlite3.register_adapter(datetime, to_db) + + conn: Connection = sqlite3.connect(db_file) + conn.row_factory = sqlite3.Row + + return conn +``` + +Then I can run write operations with `datetime` objects knowing they will be +correctly serialized. + +```python +with self.conn: + query = "update sessions set active = :active, end_time = :end_time where active = 1;" + self.conn.execute( + query, + {"active": 0, "end_time": session.end_time}, + ) +```