Skip to content

cooklang-bindings

Python bindings for cooklang-rs, the official Rust implementation of the Cooklang recipe markup language.

This is a binding, not a fork

The package contains no parser logic. Everything under cooklang/_generated/ is produced by UniFFI from the interface upstream already maintains — the same interface behind their Swift and Kotlin bindings — and the rest is a thin Pythonic layer over that output. See Relationship to cooklang-rs.

Install as cooklang-bindings, import as cooklang:

pip install cooklang-bindings

In one screen

>>> import cooklang
>>> recipe = cooklang.parse("""
... ---
... title: Test
... servings: 4
... ---
...
... == Prep ==
...
... Chop the @onion{1}.
...
... > A note that is not a step.
...
... Fry it in a #pan{} for ~{5%minutes}.
... """)
>>> recipe.title
'Test'
>>> recipe.servings
4
>>> recipe.method
('Chop the onion.', 'Fry it in a pan for 5 minutes.')
>>> recipe.sections[0].name
'Prep'
>>> recipe.notes[0].text
'A note that is not a step.'
>>> recipe.ingredients[0]
Ingredient(name='onion', quantity=Quantity(value=1, unit=None, text='1'), note=None, recipe_reference=None)
>>> recipe.cookware[0].name
'pan'
>>> str(recipe.timers[0])
'5 minutes'

What is covered

The package wraps the whole of upstream's exported UniFFI surface — all 28 functions — not just the recipe parser.

Area Entry point Guide
Recipes parse Working with recipes
Totalling ingredients combine_ingredients Working with recipes
Aisle configuration and common names parse_aisle_config Aisle configuration
Shopping lists parse_shopping_list Shopping lists
The checked log parse_checked_log Shopping lists
Quantity values parse_value, format_value Quantity values

Canonical Cooklang only

cooklang-rs parses a superset of canonical Cooklang. These bindings parse the canonical spec only, and the extensions cannot be switched on — upstream's UniFFI layer hardcodes the canonical parser. Because Cooklang reads unrecognised syntax as text rather than rejecting it, extended markup ends up inside your data rather than raising. If you ingest recipes you did not write, read Syntax extensions first.

Design notes

  • The model is plain data. Every type in cooklang.models is a frozen dataclass with no FFI object inside it, so instances compare, hash, pickle and hand to a template without surprises.

    >>> import cooklang
    >>> one = cooklang.parse("@salt{1%tsp}").ingredients[0]
    >>> two = cooklang.parse("@salt{1%tsp}").ingredients[0]
    >>> one == two
    True
    >>> len({one, two})
    1
    
  • Display text comes from upstream. A Quantity carries both a value for arithmetic and a text that preserves how the recipe wrote it — 1/2 stays 1/2 rather than becoming 0.5.

    >>> quantity = cooklang.parse("@butter{1/2%cup}").ingredients[0].quantity
    >>> quantity.value
    0.5
    >>> quantity.text
    '1/2 cup'
    
  • Sections and notes are first class. They are types upstream models directly, not something reconstructed from rendered text.

    >>> recipe = cooklang.parse("One.\n\n> A note.\n\nTwo.\n")
    >>> [type(block).__name__ for block in recipe.sections[0].blocks]
    ['Step', 'Note', 'Step']
    

Licence

MIT, matching upstream. Distributed wheels contain a compiled copy of the MIT-licensed cooklang-rs. See Relationship to cooklang-rs.