Architecture
July 29, 2026 · View on GitHub
FuncToWeb is the top of three layers, each of which solves a different problem.
pytypehint types, validation, defaults, argument construction
↓
pytypehintweb web plan, browser widgets and transport
↓
FuncToWeb routes, execution, integration and documentation
pytypehintcompiles a signature into aSignature: which types each parameter accepts, which constraints and defaults apply, and how the real arguments are built from validated values.pytypehintwebturns that contract into a plan: the description of the form that the browser consumes, with its JSON transport, plus the widgets that render it.- FuncToWeb adds what is still missing in order to publish it: the HTTP routes,
execution, file storage, outputs and
/doc.
FuncToWeb does not validate on its own, and it does not reinterpret the type
catalog. What it does is re-export that catalog (the constraint and annotation
atoms, Color, Email and the errors), so whoever writes a function never
needs to import from the lower layers.
From the function to the response
Each WebFunction compiles its schema, its plan and its base HTML once, at
build time. An inconsistent definition fails right there, before the first
request is accepted, rather than in production.
GET /{slug}/ → base HTML, or an opening with prefill
POST /{slug}/invoke
→ decode() interprets the transport, and inside it the
file_resolver swaps each reference for its local path
→ schema.build() validates and builds the arguments
→ function
→ return contract (Download or OpenForm, if it declares them)
→ outputs
Both execution endpoints share that whole path; the only difference is how the response travels. See http.md and streaming.md.
A synchronous function does not run on the event loop but in a thread
(asyncio.to_thread): otherwise a blocking call would stall the loop, and with
it every other request the server is handling. See
streaming.md.
The file_resolver is the only piece of that path that belongs to FuncToWeb,
and it is the same resolver in both endpoints and in the prefill: decode()
decides where a file is, walking the structure to any depth, while
FuncToWeb decides what counts as a valid file for this server. Neither
layer duplicates the other's work.
Where it is mounted
A function's page is a complete .pth-root: pytypehintweb requires that root
for its widgets, and FuncToWeb puts it on the <body> rather than on an inner
container, so that page and form are a single surface with a single theme.
The theme belongs to this layer for the same reason: whoever serves the document
is the only one who can settle it before the first paint. pytypehintweb
neither chooses nor stores it (it only declares what data-pth-theme means),
and the core does not know it exists. See router.md and
static-assets.md.
What counts as public contract
From a WebFunction you can read schema, plan and html, plus its
normalized metadata. The plan is also what /doc publishes, so a client reads
the same contract the browser consumes. That, together with the names
func_to_web exports, is the stable, importable API: the surface you can build
against.
Resolving the return value is not part of that contract.
WebFunction.return_parser holds what Download and OpenForm declared, and
WebFunctions.forms holds which function each already-resolved opening jumps
to. Both fields have public names and can be inspected, because the router
needs them, but a public name is not an importable API. Their types,
ReturnParser and FormAction, and the has_download() that queries them,
live in their own modules and are not re-exported from func_to_web; neither
are parser_of(), space_of() or has_file(). None of them can be imported
from func_to_web, and what the two fields expose is internal state that the
implementation leaves visible, not something published as a contract.
The reason is not the prefix → design/architecture.md.
Related: web-function.md, api-docs.md, types.md.