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
Metadata
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 | Provenance | Actions |
|---|---|---|---|---|
| tals-0.4.0.tar.gz | 311.5 kB | Without provenance | Details for tals-0.4.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| tals-0.4.0-py3-none-any.whl | Python 3 | none | any | Details for tals-0.4.0-py3-none-any.whl |
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
|
Security for tals 0.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.