Puckling
A Python port of Facebook Duckling, scoped to English and Arabic.
Puckling parses natural-language English and Arabic into structured values: numbers, ordinals, dates, durations, distances, temperatures, money, emails, URLs, phone numbers, and more.
The library has minimal dependencies (regex for PCRE-compatible Unicode patterns).
Installation
pip install puckling
Usage
import datetime as dt
from puckling import (
AmountOfMoneyValue,
Context,
Lang,
Locale,
Options,
TimeValue,
parse,
)
ctx = Context(
reference_time=dt.datetime(2013, 2, 12, 4, 30, tzinfo=dt.UTC),
locale=Locale(Lang.EN),
)
for entity in parse("I'll meet you tomorrow at 5pm for $50", ctx, Options()):
match entity.value:
case TimeValue() as tv:
print(entity.body, "→", tv.start_datetime(), "to", tv.end_datetime())
case AmountOfMoneyValue(value=amount, currency=currency):
print(entity.body, "→", amount, currency)
Switch Locale(Lang.EN) to Locale(Lang.AR) for Arabic input.
entity.value is one of the typed *Value dataclasses (AmountOfMoneyValue,
DistanceValue, TimeValue, …) re-exported from puckling. Narrow with
isinstance, match, or by passing dims=("amount_of_money",) to filter the
parse to a single dimension. For TimeValue, start_datetime() and
end_datetime() cover the instant / closed-interval / open-interval cases
without an isinstance ladder; either may be None for an unbounded side.
Latent matches
Some inputs are only entities under a charitable reading. parse("on the 5th", …) returns just an ordinal by default. With with_latent=True it also
surfaces a time entity for "the 5th of the next month" — flagged
latent=True so callers can demote it:
parse("on the 5th", ctx, Options()) # → [Ordinal(5)]
parse("on the 5th", ctx, Options(with_latent=True)) # → [Time(2013-03-05, latent=True)]
Supported dimensions
| Dimension | EN | AR | Notes |
|---|---|---|---|
| Numeral | :white_check_mark: | :white_check_mark: | Cardinals, decimals, Arabic-Indic digits |
| Ordinal | :white_check_mark: | :white_check_mark: | |
| Time | :white_check_mark: | :white_check_mark: | Dates, clock times, holidays, intervals |
| Duration | :white_check_mark: | :white_check_mark: | |
| Distance | :white_check_mark: | :white_check_mark: | |
| Temperature | :white_check_mark: | :white_check_mark: | |
| Quantity | :white_check_mark: | :white_check_mark: | |
| Volume | :white_check_mark: | :white_check_mark: | |
| AmountOfMoney | :white_check_mark: | :white_check_mark: | |
| :white_check_mark: | :white_check_mark: | Locale-agnostic | |
| URL | :white_check_mark: | :white_check_mark: | Locale-agnostic |
| PhoneNumber | :white_check_mark: | :white_check_mark: | |
| CreditCardNumber | :white_check_mark: | :white_check_mark: | Locale-agnostic |
Locale-agnostic dimensions (Email, URL, CreditCard) match across both
Lang.ENandLang.ARcontexts.
Architecture
Puckling mirrors Duckling's parsing model in idiomatic, functional Python:
- Rules are pure data:
Rule(name, pattern, prod). - Patterns are tuples of
RegexItem(regex over source text) andPredicateItem(predicates over existing tokens). - Productions are pure functions
tuple[Token, ...] → Token | None. - The engine is a saturating fixed-point parser that applies rules iteratively until no new tokens appear.
- Resolution is context-aware (reference time, locale) and dimension-specific.
All public types are @dataclass(frozen=True, slots=True) — no mutation. Parsed entity values are structured runtime dataclasses; access fields directly. Cross-dimension references go through predicates (is_numeral, is_grain, …), never imports, so each rule file stays independent.
Engine budgets
The saturating fixed-point parser is bounded by three caps to prevent runaway parses on pathological compositional inputs:
Options field |
Default | Disable with |
|---|---|---|
parse_timeout_ms |
2000 |
None |
max_tokens |
10000 |
n/a |
max_iterations |
50 |
n/a |
When any cap is hit, the engine returns the tokens it has accumulated so far (a valid, possibly partial parse). For offline corpus runs where you want unbounded analysis, pass Options(parse_timeout_ms=None).
Running scripts safely
Inline smoke tests should always be wrapped with the shell timeout so a runaway parse can't survive the calling shell:
timeout 5 uv run python -c "
from puckling import parse, Context, Locale, Lang, Options
import datetime as dt
ctx = Context(reference_time=dt.datetime.now(dt.UTC), locale=Locale(Lang.EN))
print(parse('tomorrow at 5pm', ctx, Options()))
"
The engine's own budget should be enough on its own, but the shell-level timeout is belt-and-suspenders against any future engine path that bypasses the budget check.
Development
- Requires Python 3.13+.
- Requires
uvfor dev dependencies.
uv sync --all-extras
uv run pytest
Adding a dimension or locale
To port a Duckling rule file, add:
src/puckling/dimensions/<dim>/<lang>/__init__.py
src/puckling/dimensions/<dim>/<lang>/rules.py # exports RULES: tuple[Rule, ...]
src/puckling/dimensions/<dim>/<lang>/corpus.py # exports CORPUS: tuple[Example, ...]
tests/dimensions/test_<dim>_<lang>.py
The registry auto-discovers any <dim>/<lang>/rules.py exporting RULES. No central registration list to update.
License
Apache-2.0, mirroring upstream Duckling.
Metadata
Release files for puckling 0.5.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 | |
|---|---|---|---|
| puckling-0.5.0.tar.gz | 222.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| puckling-0.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 421.1 kB
Release files / puckling-0.5.0.tar.gz
| Download URL | puckling-0.5.0.tar.gz |
|---|---|
| Size | 222.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
869712df2fb424ca48d79dfae84a8c67c1a70123c8db5260e490e1471f751fc7
|
|
BLAKE2b-256 checksum How to use checksums |
dba43362fe65041fa76a1295c1df9459f103a07095eb32f25f455788b3f6fedf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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 May 6, 2026.
Transparency logRelease files / puckling-0.5.0-py3-none-any.whl
| Download URL | puckling-0.5.0-py3-none-any.whl |
|---|---|
| Size | 198.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5da39b16076d65bdbfb9651bcf992bc4d90858b6eabc804a421b6f6be2e48456
|
|
BLAKE2b-256 checksum How to use checksums |
d5c3fe26750ba10ffd3307ae257f3bce1fd9e8365a7cefd05da5a6474f798205
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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 May 6, 2026.
Transparency log