Skip to main content

tals — Time-Aware List Slicer

logo1

tals makes time-based slicing as natural as ordinary indexing. It extends Python's [start:end] syntax with datetime and calendar-period bounds, and works on any list of objects or dicts — it only needs to k now which attribute or key holds the timestamp.


Filtering a list of objects by time period is something every Python project does — and it never gets less tedious:

# Without tals
now = datetime(2026, 3, 10)
start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
end = (start + relativedelta(months=1))
february = [e for e in entries if start <= e.created_at < end]

tals collapses this to a single expressive call:

# With tals
slice_objects(entries, "-1M:0M", "created_at", now)

It extends Python's familiar [start:end] syntax with datetime and calendar-period bounds. Any list, any object — it only needs to know which attribute or key holds the timestamp.

from tals import slice_objects

slice_objects(entries, "-7d:",    "created_at", now)  # last 7 days
slice_objects(entries, "0M:+1M", "created_at", now)  # this calendar month
slice_objects(entries, "-1W:0W", "created_at", now)  # last full week
slice_objects(entries, "0Y:",    "created_at", now)  # since Jan 1

Installation

pip install tals

Overview

Three new bound types extend standard integer indexing:

  • Time-delta bounds — relative to a reference datetime: -7d, +2h, -30m
  • Calendar-period bounds — snapped to period boundaries: 0M, -1W, 0Y
  • Literal timestamp bounds — an absolute ISO-8601 point in time: 2026-01-01, 2026-01-01T14:30:00+02:00

Bounds can be freely mixed: a temporal start can pair with an integer end, or vice versa. The library never calls datetime.now() — you supply the reference, so behaviour is always deterministic and testable.

API

def slice_objects(
    objects: list[Any],
    position: str,
    timestamp_key: str,
    reference_dt: datetime,
    week_start: int = 0,
    presorted: bool = False,
    inclusive_end: bool = False,
) -> Any | list[Any]:
Parameter Description
objects List of objects or dicts in any order
position Slice expression string (see syntax below)
timestamp_key Attribute name (objects) or key (dicts) holding the datetime value
reference_dt Anchor for all relative expressions — never inferred from the system clock
week_start First day of the week: 0 = Monday (default), 6 = Sunday
presorted Skip sorting if the list is already in ascending timestamp order
inclusive_end Make the end bound inclusive (default False)

Return value: a single-index expression returns one object (or None if out of bounds); a slice expression returns a list (possibly empty).

The list is stable-sorted ascending by timestamp_key before slicing. All indices operate on this sorted list.

Syntax

[index]
[start:end]
[start:]
[:end]
[:]

Bound types

Form Description Example
0, 1, -1 Integer index — same semantics as Python [-1] → last object
-7d, +2h, -30m Time-delta — relative to reference_dt [-7d:] → last 7 days
0M, -1M, +1M Calendar month — start of the Nth month [0M:+1M] → this month exactly
0W, -1W, +1W Calendar week — start of the Nth week [-1W:0W] → last week exactly
0Y, -1Y, +1Y Calendar year — Jan 1 of the Nth year [0Y:+1Y] → this year exactly
2026-01-01, 2026-01-01T14:30, 2026-01-01T14:30:00+02:00 Literal timestamp — any ISO-8601 date or datetime, accepted by datetime.fromisoformat [2026-01-01:2026-02-01] → January exactly

Period 0 is the period containing reference_dt; -1 is the previous period; +1 is the next.

Calendar and literal-timestamp bounds are not valid as a single index — they only make sense in a slice.

A literal timestamp with no offset (2026-01-01T14:30) is naive; if reference_dt is timezone-aware, it inherits reference_dt's timezone, the same way calendar boundaries are computed in reference_dt's timezone. A literal timestamp with an explicit offset or Z is aware and used as given — it does not need to match reference_dt's offset, since Python compares differing aware timezones correctly.

Inclusivity

By default, bounds follow Python's convention: start is inclusive, end is exclusive.

Expression Meaning
[-7d:] timestamp >= reference_dt − 7 days
[:-7d] timestamp < reference_dt − 7 days
[-1M:0M] timestamp >= start of last month and < start of this month

Pass inclusive_end=True to include the end boundary:

Expression Default (inclusive_end=False) With inclusive_end=True
[-1M:0M] up to but not including Mar 1 up to and including Mar 1
[-7d:-1d] up to but not including the -1d mark up to and including the -1d mark
[0:2] indices 0 and 1 indices 0, 1, and 2
[:-1] all but the last all items

Mixed bounds

A slice can mix bound types. Temporal bounds are resolved to datetime thresholds first, the list is filtered, then integer bounds are applied to the filtered result.

# March entries, excluding the last one
slice_objects(entries, "0M:-1", "created_at", reference_dt)
# → filter to [Mar 1, Apr 1), then apply [:-1]

# From 7 days ago, keep only the first three
slice_objects(entries, "-7d:3", "created_at", reference_dt)
# → filter to [now - 7d, …), then apply [:3]

# Between two known absolute timestamps
slice_objects(entries, "2026-01-01:2026-02-01", "created_at", reference_dt)
# → filter to [Jan 1, Feb 1)

Examples

Given four objects with a start attribute and reference_dt = 2026-03-10:

Object start
A 2026-01-10
B 2026-02-05
C 2026-03-01
D 2026-03-10
slice_objects(objs, "[-1]",      "start", ref)  # → D
slice_objects(objs, "[0]",       "start", ref)  # → A
slice_objects(objs, "[:]",       "start", ref)  # → [A, B, C, D]
slice_objects(objs, "[:-1]",     "start", ref)  # → [A, B, C]
slice_objects(objs, "[0M:]",     "start", ref)  # → [C, D]          March onward
slice_objects(objs, "[0M:+1M]",  "start", ref)  # → [C, D]          March exactly
slice_objects(objs, "[-1M:0M]",  "start", ref)  # → [B]             February exactly
slice_objects(objs, "[-1M:]",    "start", ref)  # → [B, C, D]       Feb 1 onward
slice_objects(objs, "[0Y:+1Y]",  "start", ref)  # → [A, B, C, D]    2026 exactly
slice_objects(objs, "[0M:-1]",   "start", ref)  # → [C]             March, drop last
slice_objects(objs, "[2026-02-01:2026-03-05]", "start", ref)  # → [B, C]  Feb 1 – Mar 5

Inclusive end

Add inclusive_end=True when items exactly on the boundary should be included. A common case is querying a closed interval between two known timestamps:

# Events from Feb 5 through Mar 1 inclusive — useful when Mar 1 is a known event
slice_objects(objs, "-1M:0M", "start", ref)                       # → [B]       Mar 1 excluded
slice_objects(objs, "-1M:0M", "start", ref, inclusive_end=True)   # → [B, C]    Mar 1 included

It applies equally to integer ends, where it behaves like Ruby-style range slicing:

slice_objects(objs, "0:2",  "start", ref)                        # → [A, B]     index 2 excluded
slice_objects(objs, "0:2",  "start", ref, inclusive_end=True)    # → [A, B, C]  index 2 included

slice_objects(objs, ":-1",  "start", ref)                        # → [A, B, C]  last excluded
slice_objects(objs, ":-1",  "start", ref, inclusive_end=True)    # → [A, B, C, D]  last included

Timezone handling

reference_dt and all object timestamps must be either all timezone-aware or all timezone-naive — mixing the two raises TypeError. Objects with different (but both aware) timezones are compared correctly by Python and are fully supported.

Calendar boundaries are computed in the timezone of reference_dt, so 0M on a UTC+02:00 reference resolves to midnight of the 1st in that timezone.

Out of scope

tals is a pure slicing primitive. The following are the caller's responsibility:

  • Pre-filtering — pass only the subset of objects that should be considered
  • Field extraction — read attributes from the returned objects
  • Fallback values — convert None or [] to domain-specific defaults

License

MIT

Release files for tals 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tals 0.3.0
File Size Uploaded
tals-0.3.0.tar.gz 308.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for tals 0.3.0
File Interpreter ABI Platform
tals-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 318.3 kB

Release files / tals-0.3.0.tar.gz

Download URL tals-0.3.0.tar.gz
Size 308.4 kB
Tags Source
SHA-256 checksum
How to use checksums
22e3bb2541bb6aec1b3940960921a5d0d485b8d0370b507a575328e371519e34
BLAKE2b-256 checksum
How to use checksums
d2676cfe01c276cd27a05164563d225379dd67e076c7799556503c3feb938d66
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.4

Release files / tals-0.3.0-py3-none-any.whl

Download URL tals-0.3.0-py3-none-any.whl
Size 9.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cd985e0793ceb149141e64453b3e9e17fe685a0127569bb69603b5b700514077
BLAKE2b-256 checksum
How to use checksums
487a3a48ff411accae81ac8fdc32b79cb1d0d7829507fa0566fc999d4821409f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.4

Release history Release notifications | RSS feed

0.4.0

2 release files

This release

0.3.0 This release

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page