Skip to main content

thaler

The Thaler API from Python: financial data from SEC filings, with the source filing for every value.

pip install thaler
import thaler

client = thaler.Thaler()  # reads THALER_API_KEY; or Thaler(api_key="thaler_…")

rows = client.metrics("AAPL", period="annual", keys=["revenue", "net_income"])
for row in rows.data:
    source = row.read_from.accession_number if row.read_from else None
    print(row.end_date, row.metric_key, row.value, source)
# 2025-09-27 revenue 416161000000 0000320193-25-000079

Every figure is a decimal.Decimal, every day a datetime.date, and every answer a Response with the request ID and the account’s limits beside its data. Create a key at thaler.sh/developers/keys.

What the client does

At most four requests are in flight at once. When the minute’s sixty are spent, the next request waits for the reset. A 429 is retried after its Retry-After; one longer than a minute, the month’s limit, raises RateLimitError at once.

A 500, 502, 503 or 504, or a connection that failed, is retried twice after a growing pause; those answers don’t count against the month. A 400, 401 or 404 is raised as it stands, and a Screener clause the API would refuse is a ValueError before anything is sent.

Figures are Decimal (a float loses digits past the fifteenth), days are date, instants are datetime, and each model is a frozen dataclass with None for what is absent or null. An answer that is not as documented raises DecodeError, naming the field and the request ID.

iter_screen, iter_holders, iter_holder_positions and iter_insider_filings yield rows across pages.

NotFoundError, AuthenticationError, BadRequestError, RateLimitError and ServerError are each an APIError with the problem’s code, detail and request_id.

The client

client = thaler.Thaler(
    api_key=None,          # or THALER_API_KEY
    timeout=30.0,          # seconds to wait for an answer
    max_retries=2,         # tries after the first, on 429, 500, 502, 503, 504 or a lost connection
    max_concurrent=4,      # requests in flight; the API allows four
    max_retry_after=60.0,  # the longest Retry-After waited for
)

with thaler.Thaler() as client: closes the connections on the way out. thaler.AsyncThaler is the same client for asyncio: every method awaited, every iter_* an async iterator.

Every call

Call Answers with
search_securities(query) list[SecuritySearchHit]
profile(ticker) SecurityProfile
metrics(ticker, period=, keys=, collapse=, limit=, as_of=) list[MetricValue]
metric_catalog() list[MetricCatalogEntry]
metric_lineage(ticker, metric_key, metric_value_id=, limit=, as_of=) list[MetricLineage]
metric_revisions(ticker, metric_key, metric_value_id=, fiscal_year=, fiscal_period=, limit=) list[MetricRevision]
raw_concepts(ticker, limit=) list[RawConcept]
filings(ticker, forms=, items=, limit=) list[Filing]
filings_day(date=) FilingsDay
filing(accession) FilingSource
insider_activity(ticker=None, tickers=, since=, kind=, limit=, offset=) InsiderActivityPage
search_holders(query=, limit=) list[HolderHit]
holder(cik, limit=, offset=) Holder
holders(ticker, limit=, offset=) SecurityHolders
segments(ticker, period=) Segments
prices(ticker, range=, from_=, to=) Prices
screen(where=, sort=, dir=, limit=, offset=, columns=) list[ScreenRow]
release() Release

Each returns a Response: .data as above, .meta (the route, the parameters as the API read them, the Screener’s counts, the release), .request_id, .etag, .rate_limit and .headers.

The Screener

from thaler import where

big_and_profitable = client.screen(
    where=[where("revenue", ">=", 10_000_000_000), where("net_margin", ">", 0.2)],
    sort="revenue",
    dir="desc",
    columns=["revenue", "net_margin", "market_cap"],
)
for row in big_and_profitable.data:
    print(row.ticker, row.revenue, row.net_margin)
print(big_and_profitable.meta.total, "matches")

for row in client.iter_screen(where=[where("fcf_margin", ">=", 0.15)], limit=1000):
    ...

where writes a clause as the API takes it (revenue:gte:10000000000, never scientific notation); a clause written by hand works as well.

Point-in-time reads

then = client.metrics("KHC", period="annual", keys=["net_income"], as_of="2019-03-01")
now = client.metrics("KHC", period="annual", keys=["net_income"])

Each value comes from the latest filing on or before the day, so a backtest only sees what was public at the time. See the guide on point-in-time data.

Errors

try:
    client.profile("ZZZZ")
except thaler.NotFoundError as error:
    print(error.code, error.detail, error.request_id)
except thaler.RateLimitError as error:
    print("wait", error.retry_after, "seconds;", error.violated_policies)
except thaler.APIError as error:
    print(error.status, error.code)
except thaler.TransportError:
    print("no answer")

Prices

Prices include IEX’s last sale for each trading day. Data provided for free by IEX. By accessing or using IEX Historical Data, you agree to the IEX Historical Data Terms of Use.

Versions

The SDK is 0.x while the API is in beta. thaler.API_VERSION names the API document a release follows. Changes are in CHANGELOG.md, shipped with the package.

Metadata

Release files for thaler 0.1.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 thaler 0.1.0
File Size Uploaded
thaler-0.1.0.tar.gz 33.5 kB Details

Built distribution (wheel)

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

Total release size: 70.7 kB

Release files / thaler-0.1.0.tar.gz

Download URL thaler-0.1.0.tar.gz
Size 33.5 kB
Tags Source
SHA-256 checksum
How to use checksums
19677a43fa42f213acc507d7d22ea08d634df6c698adce0afbbc5f3b77500482
BLAKE2b-256 checksum
How to use checksums
5007393aa3844ddb0c1a443b7d7c379afa419e5a79893aed6cae4d187d5f9dfd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release files / thaler-0.1.0-py3-none-any.whl

Download URL thaler-0.1.0-py3-none-any.whl
Size 37.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0b9283857c581593b5b42c319901af6b47dbb64a3afb353bae233b613fa851ff
BLAKE2b-256 checksum
How to use checksums
5c942654c45997f06598e63edba316fcdb4d7ed2bfee9a90eb9f9375920cceb0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

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