tals — Time-Aware List Slicer
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
Noneor[]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)
| File | Size | Uploaded | |
|---|---|---|---|
| tals-0.3.0.tar.gz | 308.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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
|