# Types and validation
The type hints are the contract: every value is validated before the function runs, and the function receives real Python values.
from typing import Annotated
from func_to_web import Max, Min
def percentage(value: Annotated[int, Min(0), Max(100)]) -> int:
return value
value is always an int from 0 to 100: -1, 1.5, "50" or True never
reach the function. Over HTTP a value that breaks the contract is a 422 with
the reason.
# What a parameter can be
| Type | In the form |
|---|---|
int, float, str, bool |
a number, a text, a switch |
date, time |
a date or time picker |
an Enum or Literal[...] |
a dropdown |
list[T] |
a list of fields, with add and remove |
tuple[T, ...] |
the same list; the function gets a real tuple |
tuple[T, T, T] |
a list of exactly that many fields, all of one type |
a dataclass or an @immutable model |
its fields, nested; the function gets a real instance |
T | None |
the field with a toggle that decides whether it is sent |
A | B |
a choice between the shapes of each branch |
Color, Email |
a str with a color picker, or checked as an email |
a str with FileHint |
a file picker; see Files |
Validation is recursive: it reaches every item of a list and every field of a nested dataclass.
def average(
values: Annotated[list[Annotated[int, Min(0), Max(100)]], Min(1), Max(20)],
) -> float:
return sum(values) / len(values)
Between 1 and 20 values, each from 0 to 100: the outer Min/Max count the
list, the inner ones check each item.
# Tuples and immutable models
A tuple is a list whose size can be part of the type. tuple[T, ...] takes
Min/Max like a list, and a fixed tuple whose positions are all the same
type is a list of exactly that size: with a default its rows are there and can
be neither added nor removed, and without one the user adds them.
@immutable declares a dataclass that validates itself and cannot change
afterwards. It holds its collections in tuples, and as a parameter it is a
nested form like any dataclass:
from typing import Annotated
from func_to_web import Max, Min, immutable
Channel = Annotated[float, Min(0.0), Max(1.0)]
@immutable
class Layer:
name: Annotated[str, Min(1)]
color: tuple[Channel, Channel, Channel] = (0.0, 0.0, 1.0)
tags: tuple[str, ...] = ()
def describe(layer: Layer) -> str:
return f"{layer.name}: {layer.color}"
describe receives a real Layer, already validated, whose color and tags
are real tuples.
# Constraints and presentation
Everything goes inside Annotated, and everything is imported from
func_to_web.
These constrain the value:
| Atom | What it checks |
|---|---|
Min(value, exclusive=False), Max(...) |
a number, date or time's bound; a text's or list's length. exclusive=True leaves the bound out |
MultipleOf(value) |
an int that is a multiple of value |
Choices(values=(...)) |
one of a fixed set |
Pattern(value, message=None) |
a text matching a regular expression; message is the error shown |
FileHint(extensions=(), min_size=None, max_size=None) |
a file; see Files |
These only change how the field looks:
| Atom | What it does |
|---|---|
Label(value), Description(value), Placeholder(value) |
the field's name, its help text, and a hint inside it |
Step(value) |
the step of a number's arrows |
Slider(show_value=True) |
a slider instead of a number box |
Rows(value) |
a multi-line text box of that height |
IsPassword() |
a hidden text |
OptionalToggle(enabled) |
whether an X | None field starts on |
Extra(key, value) |
a namespaced pair stored on the field, for your own code; FuncToWeb ignores it |
Color and Email are str with the patterns COLOR_PATTERN and
EMAIL_PATTERN, which you can also use yourself. A signature that cannot be
compiled raises SchemaTypeError (a wrong type) or SchemaValueError (a wrong
value, such as a default outside its bounds) when the application is built.
# Defaults
Defaults are checked when the application starts, and rebuilt for every call, so a list or a dataclass used as a default is never shared between two calls:
from dataclasses import dataclass
@dataclass
class Item:
value: int
def process(items: list[Item] = [Item(1), Item(2)]) -> int:
return sum(item.value for item in items)
# Every control, on screen
The widgets are pytypehintweb's, and its demo shows every one of them. Install
the demo extra of pytypehintweb and run:
pytypehintweb-demo

The complete catalog of types and atoms is in pytypehint's documentation.
# Limits
- Two atoms that need different controls cannot be combined:
RowswithChoicesorIsPassword, andSliderorChoiceswithPlaceholder, orChoiceswithSlider. - A file field takes no text atoms (
Pattern,Placeholder,Rows...). - An
intbeyond JavaScript's safe range (±2⁵³−1), a dataclass with no fields and a dataclass that contains itself are rejected when the application is built. So is afloatdefault so small or so large that JavaScript writes it with an exponent (1e-7,1e21), on a field withoutChoices. - A tuple whose positions differ, such as
tuple[int, str], has no form.