Form data parsing and binding — URL-encoded and multipart.
ImplementsMultiValueMappingfor consistent access across
Headers, QueryParams, and FormData.
form_from()provides lightweight dataclass binding: define a
frozen dataclass, pass it toform_from(request, MyForm), and get
a populated instance. No magic validation — just binding with type
coercion forstr, int, float, bool, datetime.date,
datetime.datetime (ISO 8601), decimal.Decimal, uuid.UUID,
enum.Enum subclasses, and list[T]for repeated fields such as
checkbox groups and multi-selects.
python-multipart is an optional dependency (pip install chirp[forms]).
URL-encoded forms use stdliburllib.parse— no extra dependency.
http.forms
| Name | Type | Default | Description |
|---|---|---|---|
type
|
|
— | |
qualified_name
|
|
— | |
element_type
|
|
— | |
description
|
|
— | |
source_file
|
|
— | |
line_number
|
|
— | |
is_autodoc
|
|
— | |
autodoc_element
|
|
— | |
_autodoc_template
|
|
— | |
_autodoc_url_path
|
|
— | |
_autodoc_page_type
|
|
— | |
title
|
|
— | |
doc_content_hash
|
|
— |
Symbols on this page
Reduce an attacker-influenced filename to a safe basename.
Strips directory components (POSIX/ and Windows \), removes NUL bytes, and rejects traversal so…
An uploaded file from a multipart form submission.
Metadata (filename, content_type, size) is immutable— this is a frozen dataclass,…
Raised when a string form accessor is used for an uploaded file field.
FormDatakeeps string fields and uploads in separate maps. Reading a…
Immutable parsed form data.
ImplementsMapping[str, str] and the MultiValueMappingprotocol.
Holds both string field values and uploaded files.
__getitem__ / getreturn string…
Raised when form data cannot be bound to a dataclass.
Coerce a string toDecimal, normalizing failure to ValueError.
Decimal("nope") raises InvalidOperation (an ArithmeticError),
notValueError— so without this the…
Coerce a raw form string to anEnummember.
Tries by value first (EnumCls(value)— the common case for forms whose
<option value=...>…
Return the coercion callable for a field's resolved target type.
Looks up the fixed_COERCIONS table first, then special-cases Enumsubclasses (each subclass is…
Bind form data from a request to a dataclass instance.
Readsrequest.form()and populates the given dataclass. Fields with defaults are optional; fields without…
Collectchirp.validation rules attached via Annotatedmetadata.
A field declared asAnnotated[str, required, max_length(100)]carries its validation rules in the type-hint metadata. This walks…
Return theAnnotated metadata tuple for hint, else None.
The metadata lives directly on theAnnotatedwrapper regardless of the base type's…
Bind form data or return a ValidationError for re-rendering.
Combinesform_from() and ValidationErrorinto a single call. On success, returns the populated dataclass. On…
Extract form field values as strings for template re-population.
Accepts a dataclass instance or aMapping. Returns a flat
dict[str, str]suitable for…
Extract the base type fromX | None or plain X.
Return the item type forlist[T]hints.
Best-effort type name for binding errors.
Parse form body into FormData.
Supports:
application/x-www-form-urlencoded(stdlib, no extra dependency)multipart/form-data(requirespython-multipart)
Parse URL-encoded form data using stdlib.
Parse multipart form data using python-multipart.
File parts are streamed into aSpooledTemporaryFile
(spilling to disk pastspool_threshold) rather than buffered whole in…
_sanitize_upload_filename
function
def _sanitize_upload_filename(filename: str) -> str
Reduce an attacker-influenced filename to a safe basename.
Strips directory components (POSIX/ and Windows \), removes NUL
bytes, and rejects traversal so a multipartfilename="../../etc/passwd"
cannot escape a chosen directory. Returns"upload"when nothing usable
remains (e.g.".."or all separators).
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
filename
|
str
|
— |
UploadFile
class
An uploaded file from a multipart form submission.
Metadata (filename, content_type, size) is immutable — this
is a frozen dataclass, so rebinding any of those attributes raises
dataclasses.FrozenInstanceError. The content is backed by a stdlib
SpooledTemporaryFile held in the private _spool
field: small files stay in memory, larger ones spill to a temp file on disk
pastspool_thresholdbytes — so a multi-GB upload never lands wholly in
RAM. (A frozen dataclass forbids rebinding fields, not mutating the object
a field points to, so the spool's IO position/buffer remains usable while
the metadata stays locked.)
read() returns the full bytes; save()streams to disk in chunks and
sanitizes the destination basename against path traversal.
FormFileFieldError
class
Raised when a string form accessor is used for an uploaded file field.
FormDatakeeps string fields and uploads in separate maps. Reading a
file-only name throughform[key] / form.get(key)used to look like
a missing field (KeyError / None), which silently dropped uploads.
This error points callers atform.files[key]instead.
Inherits fromTypeError (wrong accessor kind), not KeyError(absent
key), soexcept KeyErrorhandlers do not treat a present upload as
missing.
FormData
class
Immutable parsed form data.
ImplementsMapping[str, str] and the MultiValueMappingprotocol.
Holds both string field values and uploaded files.
__getitem__ / getreturn string field values only. Uploaded files
live underfiles— using a string accessor for a file-only name raises
FormFileFieldErrorrather than reporting the field as absent.
get_list returns all string values for a key. Membership (in,
iteration,len) covers string fields only.
Usage::
form = await request.form()
username = form["username"]
avatar = form.files.get("avatar") # UploadFile or None
FormBindingError
class
Raised when form data cannot be bound to a dataclass.
_coerce_decimal
function
def _coerce_decimal(value: str) -> Decimal
Coerce a string toDecimal, normalizing failure to ValueError.
Decimal("nope") raises InvalidOperation (an ArithmeticError),
notValueError— so without this the existing
except ValueError, TypeError path in form_fromwould miss it and
the error would escape as an uncaught exception instead of a
FormBindingError.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
value
|
str
|
— |
_coerce_enum
function
def _coerce_enum(enum_cls: type[Enum], value: str) -> Enum
Coerce a raw form string to anEnummember.
Tries by value first (EnumCls(value)— the common case for forms whose
<option value=...>carries the member value), then falls back to by
name (EnumCls[value]) which is handy for string enums declared as
RED = "red"but submitted by member name. Unknown inputs raise
ValueError so form_from reports a FormBindingErrornaming the
field — consistent with the int/float path.EnumCls(value)may raise
KeyErrorfor some enum shapes, so that is caught too.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
enum_cls
|
type[Enum]
|
— | |
value
|
str
|
— |
_resolve_coercion
function
def _resolve_coercion(target_type: Any) -> Callable[[str], Any]
Return the coercion callable for a field's resolved target type.
Looks up the fixed_COERCIONS table first, then special-cases Enum
subclasses (each subclass is a distinct type, so it cannot be a static table
key). Falls back to calling the type directly (mirrors the prior behavior
for unknown types such as plain custom classes).
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
target_type
|
Any
|
— |
form_from
function
async
async def form_from(request: Any, datacls: type[T]) -> T
Bind form data from a request to a dataclass instance.
Readsrequest.form()and populates the given dataclass.
Fields with defaults are optional; fields without defaults are required.
String fields are stripped of whitespace by default.
Supportsstr, int, float, bool, datetime.dateand
datetime.datetime (ISO 8601), decimal.Decimal, uuid.UUID,
enum.Enumsubclasses (coerced by member value, then by name), and
list[T] type coercion. Missing list fields bind to []because
browsers omit unchecked checkbox groups entirely. RaisesFormBindingError
with a dict of errors for missing or invalid fields.
Usage::
@dataclass(frozen=True, slots=True)
class TaskForm:
title: str
description: str = ""
priority: str = "medium"
@app.route("/tasks", methods=["POST"])
async def add_task(request: Request):
form = await form_from(request, TaskForm)
# form.title, form.description, form.priority are populated
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
request
|
Any
|
— | A Chirp Request object (anything with an async ``.form()`` method). |
datacls
|
type[T]
|
— | A dataclass class to bind form data into. |
_extract_field_rules
function
def _extract_field_rules(datacls: type) -> dict[str, list[Validator]]
Collectchirp.validation rules attached via Annotatedmetadata.
A field declared asAnnotated[str, required, max_length(100)]carries
its validation rules in the type-hint metadata. This walks the dataclass
fields, resolves hints withinclude_extras=True (so Annotatedis
preserved rather than stripped — noteform_fromresolves them without
extras, which is why those rules are otherwise never run), and gathers the
callable metadata items as validators. Non-callable metadata (doc
strings, sentinels, etc.) is ignored, soAnnotatedcan be shared with
other tooling.
Optional nesting is unwrapped to find the Annotatedlayer in either
order — bothAnnotated[str | None, required](metadata on the outer
wrapper) andOptional[Annotated[str, required]]/
Annotated[str, required] | None(metadata on a union member) yield the
same rules. Only fields with at least one rule appear in the returned map;
a plain dataclass with noAnnotated rules yields {}, so binding
behavior is unchanged.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
datacls
|
type
|
— |
_annotated_metadata
function
def _annotated_metadata(hint: Any) -> tuple[Any, ...] | None
Return theAnnotated metadata tuple for hint, else None.
The metadata lives directly on theAnnotatedwrapper regardless of the
base type's own optionality (Annotated[str | None, required]still
exposes__metadata__here).
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
hint
|
Any
|
— |
form_or_errors
function
async
async def form_or_errors(request: Any, datacls: type[T], template_name: str, block_name: str, /, *, retarget: str | None = None, **extra_context: Any) -> T | ValidationError
Bind form data or return a ValidationError for re-rendering.
Combinesform_from() and ValidationErrorinto a single call.
On success, returns the populated dataclass. On binding failure,
returns aValidationErrorwith the errors and the raw form values
for re-population.
Usage::
result = await form_or_errors(request, TaskForm, "tasks.html", "form")
if isinstance(result, ValidationError):
return result
# result is TaskForm — proceed with validated data
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
request
|
Any
|
— | A Chirp Request object (anything with an async ``.form()`` method). |
datacls
|
type[T]
|
— | A dataclass class to bind form data into. |
template_name
|
str
|
— | Template name for the error response. |
block_name
|
str
|
— | Block name for the error response. |
retarget
|
str | None
|
None
|
Optional ``HX-Retarget`` header value. **extra_context: Additional template context passed to ``ValidationError``. |
**extra_context
|
Any
|
— |
form_values
function
def form_values(form: Any) -> dict[str, str]
Extract form field values as strings for template re-population.
Accepts a dataclass instance or aMapping. Returns a flat
dict[str, str] suitable for passing as form=...context
toValidationError.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
form
|
Any
|
— | A dataclass instance or a ``Mapping``. |
_unwrap_optional
function
def _unwrap_optional(hint: Any) -> Any
Extract the base type fromX | None or plain X.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
hint
|
Any
|
— |
_list_item_type
function
def _list_item_type(hint: Any) -> type | None
Return the item type forlist[T]hints.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
hint
|
Any
|
— |
_type_name
function
def _type_name(value: Any) -> str
Best-effort type name for binding errors.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
value
|
Any
|
— |
parse_form_data
function
async
async def parse_form_data(body: bytes, content_type: str, *, max_parts: int | None = None, max_total_size: int | None = None, spool_threshold: int | None = None) -> FormData
Parse form body into FormData.
Supports:
application/x-www-form-urlencoded(stdlib, no extra dependency)multipart/form-data(requirespython-multipart)
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
body
|
bytes
|
— | Raw request body bytes. |
content_type
|
str
|
— | The Content-Type header value. |
max_parts
|
int | None
|
None
|
Optional cap on the number of multipart parts. Exceeding it raises ``PayloadTooLarge`` (413) — the multipart-bomb guard. ``None`` (default) means unbounded for back-compat. |
max_total_size
|
int | None
|
None
|
Optional cap on the total accumulated size of multipart parts (the multipart-specific upload ceiling, distinct from the general request-body cap). Exceeding it raises ``PayloadTooLarge`` (413). ``None`` (default) means unbounded for back-compat. |
spool_threshold
|
int | None
|
None
|
Bytes a file part keeps in memory before spilling to a temp file on disk. Defaults to ``DEFAULT_SPOOL_THRESHOLD``. |
_parse_urlencoded
function
def _parse_urlencoded(body: bytes) -> FormData
Parse URL-encoded form data using stdlib.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
body
|
bytes
|
— |
_parse_multipart
function
async
async def _parse_multipart(body: bytes, content_type: str, *, max_parts: int | None = None, max_total_size: int | None = None, spool_threshold: int = DEFAULT_SPOOL_THRESHOLD) -> FormData
Parse multipart form data using python-multipart.
File parts are streamed into aSpooledTemporaryFile
(spilling to disk pastspool_threshold) rather than buffered whole in a
bytearray. max_partscaps the number of parts to defend against a
multipart bomb;max_total_sizecaps the cumulative byte size of all
parts (the multipart-specific upload ceiling).
RaisesConfigurationError if python-multipartis not installed and
PayloadTooLarge if max_parts or max_total_sizeis exceeded.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
body
|
bytes
|
— | |
content_type
|
str
|
— | |
max_parts
|
int | None
|
None
|
|
max_total_size
|
int | None
|
None
|
|
spool_threshold
|
int
|
DEFAULT_SPOOL_THRESHOLD
|
View source · /home/runner/work/chirp/chirp/site/../src/chirp/http/forms.py:1