# Outputs
What the function returns decides what the page shows:
| Return value | Output |
|---|---|
None, or an empty list |
the text Done |
a pandas or polars DataFrame, a 2D numpy array |
a table |
| a Pillow image, a matplotlib figure | an image |
a list or tuple |
one output per item, in order |
| anything else | a text, with str(value) |
def summary() -> tuple:
return "Finished", frame, image # a text, a table and an image
The annotation plays no part: -> str, -> Any or none give the same result.
Only two outputs are declared in it: a file to download and
OpenForm.
- Tables are only those three types; a list of dicts is several texts, and
pd.DataFrame(rows)makes it a table. Every cell is sent as text. - Images travel inside the response as a PNG. FuncToWeb never imports Pillow or matplotlib: a function that returns an image has imported them.
- On the page, each output has buttons to copy it, and images and tables to download them; a table downloads as CSV.
# Downloads
Mark the return with Download to offer a file instead of showing it:
from pathlib import Path
from typing import Annotated
from func_to_web import Download
def report() -> Annotated[Path, Download()]:
return Path("/tmp/report.pdf")
def invoice() -> Annotated[bytes, Download(filename="invoice.pdf")]:
return b"%PDF-1.7 ..."
It accepts a Path, a str with a path, or bytes (with a filename), and
lists of them. The value must be the declared type: returning a str where
Path was declared is an error, not a conversion. filename can also be a
function (value, index) -> str that names each file of a list.
Downloads mix with other outputs, in order:
def process() -> tuple[str, Annotated[list[Path], Download()]]:
return "Finished", [Path("/tmp/a.pdf"), Path("/tmp/b.pdf")]
The file is copied to a returns directory (the original is never moved) and
served at /returns/<reference> for returns_ttl, one hour by default.
Annotated[Path, Download()] | None is an optional download; a union of a
download with a plain type is refused, because the value cannot say which
branch it came from.
# Errors
If the function raises, the page and the API show the error:
{"error": "ZeroDivisionError: float division by zero"}. A return that breaks
what was declared, such as a Download of the wrong type, is an error too:
ReturnContractError: expected bytes for Download, got Path.
How it works inside
Rows are read with itertuples(), not values.tolist(), which would turn
an int64 column beside a float64 one into floats (1200 shown as
1200.0).
A matplotlib figure is closed with pyplot.close() after saving, when
pyplot is loaded; otherwise pyplot's global registry grows by one figure per
call.
A collection that contains itself is detected by identity and fails as
ReturnContractError: recursive output collection; the same object twice is
fine.
The download reference is the physical file name,
<id>.<date><public name>, so the route can send the public name in
Content-Disposition without keeping any state. The sweep that deletes expired
returns only removes names it can parse, because the returns directory lives in
the system's temporary directory beside other programs' files.