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 know which attribute or key holds the timestamp.
Filtering a list of objects by time period is something many Python projects do - 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,+1w,-45s - 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 | Mapping[str, datetime],
week_start: int = 0,
presorted: bool = False,
inclusive_end: bool = False,
default_anchor: str | None = None,
) -> 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 |
Reference for all relative expressions — never inferred from the system clock. A single datetime, or a mapping of anchor name → datetime (see Named anchors) |
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) |
default_anchor |
With a mapping reference_dt, the anchor used by bounds without a name@ prefix |
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, +1w, -45s |
Time-delta (s, m, h, d, w) — relative to reference_dt; a sign is required. w is exactly 7 days, not calendar-snapped |
[-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 |
Lowercase w is a plain 7-day delta; uppercase W snaps to a calendar week boundary (Monday by default, see week_start).
Period 0 is the period containing reference_dt; -1 is the previous period; +1 is the next.
The sign on a calendar bound is optional: 1M is the same as +1M, and -0M is the same as 0M (zero has no direction). Note that time-delta bounds, unlike calendar bounds, always require a sign.
The sign matters for non-zero offsets, since it picks the direction. With reference_dt in March:
| Expression | Start of slice |
|---|---|
[-1M:] |
Feb 1 — the start of the previous month, so February onward |
[1M:] |
Apr 1 — the start of the next month, so April onward (excludes all of March) |
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.
Named anchors
Every relative bound normally resolves against a single reference_dt, which
works for "relative to now" but not when the two ends of a slice are measured
from different points in time, for example:
- "from 2 days after the first entry until 1 week before now"
- "from 3 days before Easter until 1 week after Christmas"
Named anchors solve this: you supply several reference datetimes under names,
and each bound picks the one it is measured from. Pass a mapping as
reference_dt and tag bounds with name@:
slice_objects(
entries,
"easter@-3d:christmas@+1w",
"created_at",
reference_dt={"easter": easter_2026, "christmas": christmas_2026},
)
# → from 3 days before Easter up to (excluding) 1 week after Christmas
Bounds without a prefix resolve against default_anchor:
slice_objects(
entries,
"+2d:now@-1w",
"created_at",
reference_dt={"first_entry": first_entry_dt, "now": now},
default_anchor="first_entry",
)
# → from 2 days after the first entry until 1 week before now
- Anchors are only valid on time-delta and calendar bounds (
now@-7d,now@0M) — not on integer indices or literal timestamps. - A mapping with an untagged bound and no
default_anchor, or an unknown anchor name, raisesValueError(the message lists the available anchors). Literal timestamps count as untagged: a naive one takes its timezone fromdefault_anchor. - A bare
datetimeworks exactly as before and needs nodefault_anchor. - tals has no calendar or holiday knowledge: you compute
easter_2026etc. yourself (e.g. withdateutil.easter) and pass ordinary datetimes.
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. With named anchors, only the anchors a position actually references are checked. 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.4.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.4.0.tar.gz | 311.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tals-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 323.5 kB
Release files / tals-0.4.0.tar.gz
| Download URL | tals-0.4.0.tar.gz |
|---|---|
| Size | 311.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a2b5ade6e58cc7aa4609aac20b21d3d271b5d9734670b3d929b8ee80af44bb25
|
|
BLAKE2b-256 checksum How to use checksums |
b02bc4a8d98c787d6f86f1a0e570c6d2e5979eddc537ea34c26667f9a9c1a35d
|
| 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.4.0-py3-none-any.whl
| Download URL | tals-0.4.0-py3-none-any.whl |
|---|---|
| Size | 12.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e4e14e65730845d26097c7a86559778e6e6d280353c507427af821fc1c9ada2c
|
|
BLAKE2b-256 checksum How to use checksums |
ac672e1c1669a7a1381231b8e55aed4c917e8b099ed34f40b6667d2cab4adfb1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.4
|