2025, Oct 15 13:50

Derive a start date from a finish date in the CPython C API using PyDelta_FromDSU and PyNumber_Subtract

Learn how to do date arithmetic in the CPython C API: create a timedelta with PyDelta_FromDSU and subtract it via PyNumber_Subtract to default a start from end.

Working with dates at the C layer of CPython often feels under-documented compared to the high-level Python side. A common case is defaulting one date based on another — for example, treating the missing start timestamp as one day before the finish timestamp — but doing so without leaving the Python C API.

The problem in context

Suppose a native function accepts two Python objects, start and finish. If start isn’t passed, the function should derive it as “finish minus one day.” Converting non-date inputs is handled, and a one-day timedelta is created, but the missing piece is applying that delta at the C API level.

/* Attempt converting the end-date, if not a date already */
if (!PyDate_Check(endObj)) {
    tmp = PyDate_FromTimestamp(endObj);
    if (tmp == NULL)
        return PyErr_Format(PyExc_ValueError,
            "%R is not a valid date", endObj);
    endObj = tmp;
}

/* If begin-date is not specified, assume one day prior the end date */
if (beginObj == NULL) {
    PyObject *deltaOne = PyDelta_FromDSU(1, 0, 0);
    /* Derive beginObj from endObj by applying the one-day delta somehow */
}

What’s really going on

The Python C API exposes constructors for date and timedelta types, but not direct helpers for date arithmetic. That doesn’t mean it’s impossible — it just means arithmetic must be expressed through the generic number protocol. In practice, that’s the PyNumber API, which triggers the same operations that Python code uses (like calling __sub__ under the hood).

The solution: use the PyNumber API

After creating a timedelta via PyDelta_FromDSU, compute the offset with PyNumber_Subtract. The following minimal example shows a native function, called via ctypes on Windows, that returns start as-is when provided, or subtracts one day from finish when start is None. The datetime C definitions must be initialized by including <datetime.h> and calling PyDateTime_IMPORT.

cl /LD /W4 /Ic:\python313\include test.c -link /libpath:c:\python313\libs

#include <Python.h>
#include <datetime.h>

__declspec(dllexport)
PyObject* derive_begin(PyObject* begin_obj, PyObject* end_obj) {
    PyDateTime_IMPORT;
    if (begin_obj == Py_None) {
        PyObject* delta1 = PyDelta_FromDSU(1, 0, 0);
        begin_obj = PyNumber_Subtract(end_obj, delta1);
        Py_XDECREF(delta1);
    }
    return begin_obj;
}

And the tiny driver to demonstrate behavior without writing a full extension module:

import datetime as dt
import ctypes as ct

lib = ct.PyDLL('./test')
derive_begin = lib.derive_begin
derive_begin.argtypes = ct.py_object, ct.py_object
derive_begin.restype = ct.py_object

beg = dt.datetime(2025, 8, 14)
end_ = dt.datetime(2025, 8, 14, 1)

print(f'begin  = {beg}')
print(f'end    = {end_}')
print(derive_begin(beg, end_))   # returns begin
print(derive_begin(None, end_))  # returns 1 day before end

Sample output:

begin  = 2025-08-14 00:00:00
end    = 2025-08-14 01:00:00
2025-08-14 00:00:00
2025-08-13 01:00:00

Why this matters

Native code often needs to manipulate Python datetime objects without round-tripping through custom C date logic. Using PyDelta_FromDSU together with PyNumber_Subtract keeps everything inside the Python object model and avoids converting to and from struct tm or mktime-style APIs. When the datetime C API doesn’t expose arithmetic helpers, the PyNumber API is the intended bridge to invoke the same operations Python code would perform.

Takeaways

If you need date arithmetic from the Python C API, construct timedelta objects with PyDelta_FromDSU and apply them via the PyNumber API such as PyNumber_Subtract. Include <datetime.h>, call PyDateTime_IMPORT before use, and manage temporary references you create. If argument defaulting is the only requirement, another pragmatic approach is to handle that at the Python level before calling into the native routine, keeping the C side focused on core logic.

The article is based on a question from StackOverflow by Mikhail T. and an answer by Mark Tolonen.