Overview
useful-views is a Python package that gives a data project a fixed
structure, so that every derived table (a "useful view") records the recipe
that made it. Every recipe records the steps that make that recipe.
Full documentation, including a command reference and a guide for AI tools: https://jameshorine.github.io/useful-views/
Installation
uv init --bare # once, if the folder has no pyproject.toml yet
uv add useful-views polars # or, in an activated virtual environment: pip install useful-views polars
Install it into the same Python environment as the libraries your steps use
(polars, pandas, ...): steps run with the Python that runs useful-views. So
do not run it with uvx or pipx. The
installation guide
covers pip, conda, Windows and notebooks.
Usage
With the environment active (or with uv run in front of each command):
useful-views example pwt # a ready-made example; for your own data: useful-views init my-project
cd pwt-example
useful-views sync # run every recipe, upstream first, and record what each made
useful-views check # report anything missing, refused or out of date
useful-views cookbook # write cookbook.json, the recipes the project uses
The quickstart walks through it, in Python and on the command line.
The resulting data project structure
Using useful-views in a data project results in this layout:
my-data-project/
├── README.md # written by humans; documents the data project
├── AGENTS.md # written once by init: instructions for AI coding assistants
├── datapackage.json # generated: resources and recipes (never hand-edited)
├── cookbook.json # generated by useful-views cookbook: the recipes used, in run order
├── .gitignore # generated by init: ignores raw/, work/, and other data files
│
├── raw/ # raw inputs: git-ignored, never written by a recipe
│ ├── .gitkeep # keeps the folder in git
│ └── my_data.extension # any format
│
├── recipes/ # written by humans, AI tools, or both
│ ├── recipe_01/ # one folder = one recipe, at least 1 step, maybe more!
│ │ ├── recipe.toml # description, inputs, outputs
│ │ ├── 01_first_step.py # one file = at least one step; top comment is its description
│ │ ├── 02_second_step.py
│ │ └── 03_third_step.py
│ │
│ └── recipe_02/ # a second recipe, taking a view as its input
│ ├── recipe.toml # inputs = ["useful-views/recipe-01.parquet"]
│ └── 01_only_step.py # smallest valid recipe: a file containing a single step
│
├── work/ # intermediates, never recorded, git-ignored
│ ├── recipe_01/ # one scratch folder per recipe, emptied before each run
│ │ ├── step_1.extension # any format
│ │ └── step_2.extension
│ └── recipe_02/
│
└── useful-views/ # declared outputs only, flat
├── README.md # generated: catalog of the views and how each was made
├── recipe-01.parquet # produced by recipes/recipe_01
└── recipe-02.parquet # produced by recipes/recipe_02
Raw inputs and work files can be in any format. Views are parquet files:
parquet keeps each column's type, so the record in datapackage.json and
the catalog can say exactly what every view contains. Other view formats
may be allowed in the future.
Vocabulary
Using useful-views is meant to be easy! However, we have a few (hopefully) useful vocabulary terms to help our collective practice and understanding.
The building blocks are raw inputs, steps and recipes.
| Term | What it is | Where it lives |
|---|---|---|
| Raw input | A file you start from. Nothing in raw/ is produced by a recipe. |
raw/ |
| Step | One file that does at least one non-trivial action on the data. | A file in a recipe folder |
| Recipe | Like cooking: steps run in order. From the inputs it declares, it produces the useful views it declares. | A folder in recipes/ |
For example, in polars, given:
df = pl.DataFrame(
{
"foo": [-20.5, float("nan"), 80.0],
"bar": [float("nan"), 110.0, 25.5],
"ham": ["xxx", "yyy", None],
}
)
calling polars.DataFrame.drop_nans() returns:
>>> df.drop_nans()
shape: (1, 3)
┌──────┬──────┬──────┐
│ foo ┆ bar ┆ ham │
│ --- ┆ --- ┆ --- │
│ f64 ┆ f64 ┆ str │
╞══════╪══════╪══════╡
│ 80.0 ┆ 25.5 ┆ null │
└──────┴──────┴──────┘
This would be considered a step. So, again, a step is at least one non-trivial transformation action on the data. That is the minimum. There is no maximum yet: a step may do many transformations, and the author decides where one step ends and the next begins.
Work: intermediate files, in work/<recipe>/, read only by later steps
of the same recipe.
Useful view: a declared output of a recipe, in useful-views/. Every
view is produced by exactly one recipe. A view can be an input to another
recipe. Anything read from outside the recipe that made it must be a view,
not a work file.
Writing a recipe
A recipe folder holds one recipe.toml and at least one step file.
recipes/pwt_panel/recipe.toml:
description = "Country-year panel of core Penn World Table variables for Brazil, Germany, Spain and the United States."
inputs = ["raw/pwt110_bra_deu_esp_usa.csv"]
outputs = ["useful-views/pwt-panel.parquet"]
recipes/pwt_panel/01_reshape_long.py:
# Reshape the wide export (one column per year) into one row per country, variable and year.
import polars as pl
wide = pl.read_csv("raw/pwt110_bra_deu_esp_usa.csv")
long = wide.unpivot(
index=["ISO code", "Country", "Variable code", "Variable name"],
variable_name="year",
value_name="value",
).with_columns(pl.col("year").cast(pl.Int32))
long.write_parquet("work/pwt_panel/long.parquet")
The conventions:
- Inputs and outputs are written as paths from the project root. The path in
recipe.tomlis the same path the step uses in its code. - An input is a file under
raw/, or a view underuseful-views/made by another recipe. - An output is always a file directly inside
useful-views/. - Step files are named
NN_name.pyand run in numeric order. - The comment at the top of a step file is its description. It is required.
- A step is an ordinary script. It runs from the project root and does not
import
useful_views. Use whichever data library you like. - Steps pass data to each other only through files in
work/<recipe>/.
In detail:
- Recipe folder name: lowercase letters, digits and underscores, such as
pwt_panel. Create one withuseful-views new-recipe <name>; its template says TODO and is refused until you fill it in. recipe.tomlhas exactly three keys.descriptionis one sentence, required.inputsandoutputsare lists of at least one path each. Other keys are ignored with a warning.- Inputs: a file anywhere under
raw/, or a view directly insideuseful-views/that another recipe makes and has already made successfully. - Outputs: one or more
.parquetfiles directly insideuseful-views/. Each view is made by exactly one recipe. - Paths: relative to the project root, with forward slashes, matching the
letter case on disk. Write
raw/data.csv, not./raw/data.csv,raw\data.csvor an absolute path. The error message gives the corrected string where it can; inside double quotes a backslash is a TOML escape, so then it only tells you to use forward slashes. - Step files:
NN_name.py, two digits, an underscore and a name. They run in numeric order;00_and gaps in the numbering are fine. A file named almost like a step, such as1_clean.py,01-clean.py,01 clean.pyor01_clean.py.txt, is not run, andrunwarns about it. R, notebook, Quarto, Julia and Stata files named like a step (02_clean.R,02_explore.ipynb, ...) are refused: steps must be Python files. Any other file in the folder is ignored, unless it starts withNN_and ends in.py(then it is a step: keep old copies elsewhere). - Step description: the block of
#lines at the very top of the file, its lines joined with spaces into one line. Blank lines, a shebang, a coding line, notebook cell markers (# %%,# In[1]:, with any title on the same line), a Jupytext# ---header, an inline# /// scriptblock and decoration lines like# -----before it are skipped; a docstring is not, so put the comment first. It must not start with TODO. Step files are UTF-8. - Steps run one at a time as separate processes, with the project root as
the working directory, using the same Python interpreter as useful-views,
and with
work/<recipe>/already created and empty. A step gets no keyboard input, soinput()andbreakpoint()fail insiderun; to debug a step, run it yourself from the project root with the same Python, as inpython -X utf8 recipes/pwt_panel/01_reshape_long.py, after the recipe has run once (sowork/<recipe>/exists), thenrunthe recipe again to record it. A step reads its recipe's inputs and earlier steps' files inwork/<recipe>/, and writes the recipe's outputs. It never writes toraw/, and never reads another recipe's work files.
Example data
Example projects live in a separate repository,
useful-views-data, the
way seaborn's example datasets live in seaborn-data. useful-views example <name> [path] (Python: example(name, path)) downloads one into a new
folder, ready to run with useful-views sync. Each release of useful-views
pins a version of that repository and the checksum of every file, so an
example never changes under you and a damaged download is never used.
The pwt example uses an extract of the Penn World Table, version 11.0, for
Brazil, Germany, Spain and the United States. The Penn World Table is
licensed under a Creative Commons Attribution 4.0 International License; the
example's README.md gives the exact source and the citation.
Commands
| Python | Command line | What it does |
|---|---|---|
init(path=".") |
useful-views init [path] |
Create the structure above. Safe to run twice; never overwrites a file. |
example(name, path=None) |
useful-views example <name> [path] |
Download a ready-to-run example project, such as pwt. See Example data. |
new_recipe(name) |
useful-views new-recipe <name> |
Create a recipe folder with a template recipe.toml and one stub step. |
run(recipe) |
useful-views run <recipe> |
Run one recipe's steps in order and record what it produced. |
sync(all=False) |
useful-views sync [--all] |
Make the views match the recipe folders, like uv sync: run what is missing or changed, upstream first, and remove what is no longer declared. |
check() |
useful-views check |
Report everything run and sync would refuse, every recipe sync would run, and what is missing or out of place. Changes nothing. |
cookbook(check=False) |
useful-views cookbook [--check] |
Write cookbook.json, the ledger of the recipes used, in run order. --check verifies it, for CI. |
RULES |
useful-views rules |
List the numbered rules a recipe must follow. |
root() |
none | Return the project root, for building paths in a notebook. |
run and sync differ the way uv run and uv sync do: run does the one
recipe you name, now; sync brings the whole project in line with its
recipe folders.
What run does
Before changing anything on disk, run checks the recipe and how it fits
the project (see Rules). Then it:
- deletes the recipe's declared views and empties
work/<recipe>/, so a leftover file can never pass for a fresh result; - runs each step in order. Their output appears as they run, in a terminal or a notebook cell. The first step that fails stops the run;
- checks that every declared view was written as a non-empty parquet file;
- records the run: it rewrites
datapackage.jsonanduseful-views/README.md. Only this recipe's entries change; every other recipe's record is kept exactly as it was.
If a step fails or a view is missing, nothing is recorded: the recipe's
views are removed, and its record from the last successful run, if any, is
kept. check then reports the views as missing, or the recipe as never run.
The files in work/<recipe>/ stay as the steps left them, so you can see how
far the run got. Running the same recipe twice leaves datapackage.json and
the catalog byte-for-byte unchanged.
run never runs other recipes. If an input is another recipe's view, run
that recipe first, or use sync. If other recipes read the views run just
made again, it warns and names them: their views are now out of date.
What sync does
sync makes the project match its recipe folders, the way uv sync makes
an environment match its lock file. Records only ever come from real runs.
- It checks every recipe against the rules, judging each against
the records as they will be when its turn comes. If any recipe would be
refused, it lists every problem at once and changes nothing. A folder
with no
recipe.tomlis skipped with a warning. - It removes the records of recipes whose folder was deleted, and any view a recipe no longer lists in its outputs, along with those view files. It warns about recipes that read a removed view.
- It runs, upstream first, every recipe that has never run, whose
recipe.tomlor steps no longer match its record, or whose views are missing, and everything downstream of those.--all(all=True) runs every recipe. It stops at the first failure or interruption; recipes that ran before it stay recorded, and the message says what to run next.
On a fresh clone, place the raw files and run useful-views sync to make
every view. sync does not notice a change inside a step's code, a raw file
replaced under the same name, or a git pull or branch switch (the views
are not in git). After any of those, use sync --all, or run the changed
recipe and then each recipe downstream of it.
Only one command that changes a project runs at a time: a second run,
sync or cookbook started meanwhile is refused instead of overwriting the
first one's records.
Steps are code
run and sync execute every step in recipes/ as you, with your
permissions, your environment variables (including any passwords or tokens
in them) and your network access. There is no sandbox. Read the recipes of a
project you did not write, or changes someone proposes, before running them.
check, rules and cookbook never run a step.
What check reports
| Kind | Meaning |
|---|---|
unreadable-recipe |
A recipe folder whose recipe.toml is missing or cannot be read. |
refused |
A recipe breaks a rule, so run and sync would refuse it. The finding names the rule. |
missing-input |
An input a recipe.toml declares is not on disk. |
never-run |
A recipe that has never run successfully. |
out-of-date |
A recipe sync would run again: its recipe.toml or steps changed, its outputs changed, or a view it reads will be made again. |
missing-recipe-folder |
datapackage.json records a recipe whose folder is gone. sync removes the record. |
missing-view |
A recorded view whose file is missing. |
stale-cookbook |
cookbook.json no longer matches the recorded recipes. Update it with useful-views cookbook. |
stale-catalog |
useful-views/README.md does not match datapackage.json (edited by hand, or left from a merge). sync writes it again. |
stray-file |
A file or folder in useful-views/, other than README.md, that no recipe makes. |
Each finding says what to do about it, and check uses the same plan as
sync, so a clean check means sync has nothing to refuse. It does not
notice changes inside a step's code or to a file's contents.
Rules
run refuses a recipe, before changing anything on disk, when:
recipe.tomlis missing or unreadable, lacksdescription,inputsoroutputs, has an empty list, lists a path twice, or its description starts with TODO.- The recipe has no step file; a step has no top comment, or one that starts with TODO; or two steps share a number.
- A file in the recipe folder is named like a step but is an R, notebook,
Quarto, Julia or Stata file (
.R,.Rmd,.ipynb,.qmd,.jl,.do): steps must be.pyfiles. - An input is not a file under
raw/or directly insideuseful-views/, is written in the wrong form (or as a wildcard), has a name that cannot exist on Windows, does not exist with exactly that letter case, or is an empty or unreadable raw file. - An input inside
useful-views/is not made by another recipe that has already run successfully. - The recipe would create a cycle: it reads its own view, directly or through a chain of other recipes.
- An output is not a
.parquetfile directly insideuseful-views/, has a name that cannot exist on Windows, isuseful-views/README.md, or is already made by another recipe. - Recording it would give two files the same resource name (a view's file
name, or a raw file's path below
raw/, without the extension, lowercased, with/and any other ASCII character than a-z, 0-9,.,_or-turned into-; accented letters lose their accents, and letters of other scripts become their Unicode code point), such asraw/data.csvanduseful-views/data.parquet, orraw/My Data.csvanduseful-views/my-data.parquet.
run also refuses a recipe name that does not match a folder in recipes/,
or a folder name that is not allowed (lowercase letters, digits and
underscores, and not a name Windows reserves, such as con or aux). Every refusal names the rule it broke and
says what to fix, and in which file. useful-views rules lists the rules;
from Python they are useful_views.RULES. A refusal raises
RecipeRefused, whose rule is the number above (or None for the name
refusals), rule_title its title, and path the file to fix.
Using it from Python
import useful_views as uvs
uvs.run("pwt_panel") # works from any folder inside the project
uvs.check(root="~/data/my-project") # or name the project folder explicitly
gdp = uvs.root() / "useful-views" / "gdp-per-capita.parquet"
new_recipe,run,sync,check,cookbookandrootfind the project by walking up from the current folder todatapackage.json, so they work from a notebook innotebooks/. All of them butrootalso takeroot=: the project folder itself, the one holdingdatapackage.json.initandexampletake the folder to create.initandexamplereturn the project folder,new_recipethe recipe folder,runaRunResult(recipe,outputs,warnings),syncaSyncResult(removed,dropped,deleted,ran,reasons,warnings),cookbookaCookbookResult(path,written,up_to_date,recipes,findings), andchecka list of findings (kind,path,message,rule,recipe,reason).- Failures raise subclasses of
UsefulViewsError:RecipeRefused(rule,rule_title,path,refusals),StepFailed(step,returncode,stderr),OutputError(paths,problems),CookbookError(findings),ExampleError(kind),ProjectNotFoundandProjectError. Each also hasrecipe,sync_resultandto_dict(). Warnings areUsefulViewsWarning. - On the command line, the exit code is 0 for success, 1 when
checkfinds problems orcookbook --checkfinds the cookbook out of date, 2 for a usage error, 3 when an operation is refused or fails, 130 when interrupted and 143 when terminated.useful-views --versionprints the version.python -m useful_views ...works the same asuseful-views ..., for example when theuseful-viewscommand is not found.
For scripts and AI tools
--jsonon any command,--helpand--versionincluded, prints exactly one JSON document on standard output, also on errors, with any warnings in it; everything else goes to standard error.-C <path>runs a command in another folder, likegit -C.- Every error carries its facts as attributes and
to_dict(): the recipe, the rule, the file to fix, and the failed step and its error output. initwrites anAGENTS.mdinto each new project, with the rules an AI coding assistant must follow there.- In CI,
useful-views cookbook --checkneeds no data: it checks thatcookbook.jsonanddatapackage.jsonmatch the recipe folders.
Generated files
datapackage.json is a Data Package that
records what each recipe made from what. frictionless validate datapackage.json passes on it whenever every recorded file is present.
resourceslists every raw file a recorded recipe read, then every view, each with its columns and their types. A view also hasproducedBy: the recipe that made it and that recipe's inputs.recipeslists each recorded recipe with its description, inputs, outputs and steps, as they were at its last successful run.- Column types come from the files themselves: a parquet file's own types,
and for a raw CSV, types inferred from the whole file. In a raw CSV only an
empty cell counts as missing, so a column that marks missing values with
NAor.is recorded asstring. Other raw formats (.dta,.xlsx, ...), and CSVs that are not plain comma-separated UTF-8, are recorded without columns. - Never edit it by hand. If it is damaged, restore it from git.
useful-views/README.md is a catalog generated from datapackage.json:
for each view, the recipe's description, its inputs, its numbered steps, and
its columns.
cookbook.json is the ledger of the recipes the project uses, written by
useful-views cookbook the way uv lock writes uv.lock: every recorded
recipe in run order, upstream first, with its description, inputs, outputs
and steps, and the raw inputs the recipes read. To write it, the project must
be in sync. useful-views cookbook --check exits 1 when it is missing or out
of date, or when a recipe changed without a sync. It needs no data, so CI can
require an up-to-date cookbook. See
Cookbook
for what it holds and a ready-made CI job.
.gitignore is written once by init, if there is none:
raw/*
!raw/.gitkeep
/work/
useful-views/*.parquet
__pycache__/
So raw data, work files and views stay out of git, while recipes/,
datapackage.json, cookbook.json, AGENTS.md and the catalog are
committed, with the files that list your steps' libraries (pyproject.toml
and uv.lock, or requirements.txt). A teammate installs those, places the
raw files and runs useful-views sync to make the views again.
Who writes what
Humans write the project README.md and everything in recipes/; init
and new-recipe only start them from templates and never touch them again.
The package writes datapackage.json, useful-views/README.md and, when you
run useful-views cookbook, cookbook.json; never edit those by hand.
init writes .gitignore and AGENTS.md once, if they are not there; after
that they are yours. So no file has two authors at the same time.
Planned, TODO, Not yet
- Hashing. Version 1 of
cookbook.jsonrecords every recipe and its steps. Later versions will add the SHA-256 of every raw input, recipe file, step and view, and the environment the steps ran in, locking them the wayuv.locklocks a Python environment. Then useful-views can report a view as stale, andsynccan notice a change inside a step's code, a replaced raw file, or views left from before agit pull. - Whether a step should have an upper limit on how much it does.
- Recording where a raw file came from and how to fetch it again.
- More view formats alongside parquet, such as CSV and Arrow.
- Raw inputs that are folders or sets of files, such as partitioned parquet datasets and shapefiles.
- Subfolders inside
useful-views/, and notebooks as steps.
License
useful-views is released under the MIT License; see LICENSE. The example
data is not part of the package and not under its license: it lives in the
useful-views-data
repository (with a copy in tests/data/ for the tests), and is an extract of
the Penn World Table, licensed CC BY 4.0. The example's README.md gives the
citation.
Citing useful-views
If you use useful-views in your research, please cite it. GitHub's
Cite this repository button (from CITATION.cff) gives APA and BibTeX,
or use:
Horine, J. (2026). useful-views: Documented derived tables for data projects (Version 0.1.0) [Computer software]. https://github.com/jameshorine/useful-views
@software{horine_useful_views_2026,
author = {Horine, James},
title = {useful-views: Documented derived tables for data projects},
year = {2026},
version = {0.1.0},
url = {https://github.com/jameshorine/useful-views},
license = {MIT}
}
Please also cite the sources of any data you use with it; the pwt example
uses the Penn World Table, whose citation is in the example's README.md.
Metadata
Release files for useful-views 0.1.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 | |
|---|---|---|---|
| useful_views-0.1.0.tar.gz | 581.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| useful_views-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 662.9 kB
Release files / useful_views-0.1.0.tar.gz
| Download URL | useful_views-0.1.0.tar.gz |
|---|---|
| Size | 581.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3ce86749b1daa535007518f400f533b16e67782d79c4e83bd6a28ef638c10f06
|
|
BLAKE2b-256 checksum How to use checksums |
6c099288bfdfc39a87b65bf7f4d42392da1e3fbc29c271f5934a3f583920cce4
|
| 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 Oct 16, 2026.
Transparency logRelease files / useful_views-0.1.0-py3-none-any.whl
| Download URL | useful_views-0.1.0-py3-none-any.whl |
|---|---|
| Size | 81.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4fe4c33446a218b2de9be8b8548104f505a63c9fe489ddbecb5ff8982f691200
|
|
BLAKE2b-256 checksum How to use checksums |
f6e6247d72930fcb93f45e780751067447dcabdad60c8f57b61407f09808c150
|
| 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 Oct 16, 2026.
Transparency log