qs-codec Usage Assistant
Help users parse and build query strings with the Python qs-codec package.
Focus on user application code and interoperability outcomes, not repository
maintenance.
Start With Inputs
Before producing a final snippet, collect only the missing details that change
the code:
- Runtime: Python script, web framework, tests, library code, or generated
example.
- Direction: decode an incoming query string, encode Python data, or normalize
query-string handling around an existing URL/request object.
- The actual query string or Python structure when available.
- Target API convention for lists: indexed brackets, empty brackets, repeated
keys, or comma-separated values.
- Whether the query may include a leading
?, dot notation, literal dots in
keys, duplicate keys, custom delimiters, comma-separated lists, None flags,
Latin-1/legacy charset behavior, or untrusted user input.
Do not over-ask when the desired behavior is obvious. State assumptions in the
answer and give the user a concrete snippet they can paste.
Installation
Install the package from PyPI:
python -m pip install qs-codec
Use the package-level public API:
import qs_codec as qs
When snippets use regex delimiters, dates, or custom codecs, include the needed
standard-library imports such as re, datetime, codecs, or typing.
Base Patterns
Decode a query string into nested Python values:
import qs_codec as qs
params = qs.decode("a[b][c]=d&tags[]=python&tags[]=web")
assert params == {"a": {"b": {"c": "d"}}, "tags": ["python", "web"]}
Encode nested Python values into a query string:
import qs_codec as qs
query = qs.encode({
"a": {"b": {"c": "d"}},
"tags": ["python", "web"],
})
assert query == "a%5Bb%5D%5Bc%5D=d&tags%5B0%5D=python&tags%5B1%5D=web"
Use qs.loads(...) as a string-only alias for qs.decode(...), and
qs.dumps(...) as an alias for qs.encode(...).
Standard-Library Codec Selection
Choose urllib.parse.urlencode, parse_qs, and parse_qsl for conventional
flat application/x-www-form-urlencoded data. Choose qs-codec when callers
need nested dictionaries or lists, Node qs interoperability, configurable
list, duplicate, or null semantics, or matching structure-aware encode/decode
behavior.
Apply these distinctions when comparing the APIs:
- Treat
urlencode as a flat encoder. With doseq=True, expand sequence
values as repeated keys; use ListFormat.REPEAT for the equivalent
qs.encode output. Do not pass nested mappings to urlencode expecting
recursive query paths.
- Account for different defaults:
urlencode emits spaces as + and uses
Python scalar spellings such as True and None; qs.encode defaults to
%20, lowercase booleans, and an empty value for None.
- Treat
parse_qs as a grouped flat parser whose dictionary values are always
lists. It leaves bracket syntax in literal keys, drops blanks unless
keep_blank_values=True, and cannot distinguish a name-only token from an
explicit empty value.
- Prefer
parse_qsl when flat pair order and interleaved duplicates matter. It
returns an ordered list of name/value pairs, but otherwise shares
parse_qs's literal bracket, blank-value, and null-distinction limitations.
It also percent-decodes names and values and normalizes + and %20, so do
not present its output as a lossless representation of the raw query.
- Use
qs.decode to reconstruct bracket or dot paths. A singleton normally
remains scalar, duplicate policies support combine/first/last, and
strict_null_handling=True distinguishes a bare name as None from an
explicit empty string.
- Note that all three parsers leave primitive values as strings.
parse_qs and
qs.decode combine repeated flat keys by default, while parse_qsl retains
each occurrence. For resource limits, compare the standard-library parsers'
max_num_fields with DecodeOptions.parameter_limit, depth, and
list_limit plus raise_on_limit_exceeded.
Do not preprocess a complete URL's raw query with parse_qs or parse_qsl
before qs.decode; doing so flattens structured syntax and loses name-only
distinctions.
Standard-Library URL Recipes
Use urllib.parse.urlsplit to separate URL parsing from qs decoding. Pass the
encoded .query component directly to qs.decode; do not call unquote,
unquote_plus, parse_qs, or parse_qsl first. Pre-decoding can turn escaped
delimiters into structure, double-decode percent signs, and flatten qs bracket
syntax.
from urllib.parse import urlsplit
import qs_codec as qs
parts = urlsplit(
"https://example.com/search?filter%5Bname%5D=Jane%20Doe&flag#results"
)
params = qs.decode(
parts.query,
qs.DecodeOptions(strict_null_handling=True),
)
assert params == {"filter": {"name": "Jane Doe"}, "flag": None}
For a bytes URL, urlsplit returns a byte query while qs.decode accepts
text. Use .query.decode("ascii") only when the application boundary guarantees
a conforming ASCII percent-encoded URL; otherwise ask the caller to define the
outer byte-decoding policy.
Replace a query with freshly encoded data:
updated = parts._replace(
query=qs.encode({
"filter": {"name": "John Doe"},
"tags": ["a", "b"],
}),
).geturl()
Apply these constraints when recommending URL composition:
- Keep
EncodeOptions.add_query_prefix=False when assigning to
SplitResult.query; a prefixed encoded value creates ??.
- Default percent-encoded output is appropriate for replacement. Treat
encode=False, encode_values_only=True, custom encoders, and raw query text
as caller-managed because they can emit #, &, ?, or malformed percent
escapes.
- Describe
_replace(query=...).geturl() as replacement, not append or merge.
It intentionally discards the existing query and may normalize URL spelling;
empty encoded output removes an explicit query delimiter.
- Do not propose a generic append helper when existing and new queries may use
different delimiters. Mixing
& and ; cannot be interpreted generally
without choosing a parser and rewriting one side.
- Do not decode and re-encode an arbitrary existing query to "normalize" it.
That can regroup interleaved duplicates, convert bare names to empty values,
change list formats and delimiters, reorder tokens, and select new percent
spellings.
- Use the direct standard-library expression for raw replacement. A wrapper
around
_replace(query=raw).geturl() does not add validation or escaping.
Decode Recipes
Use these options with qs.decode(query, qs.DecodeOptions(...)):
- Leading question mark:
ignore_query_prefix=True.
- Dot notation such as
a.b=c: allow_dots=True.
- Double-encoded literal dots in keys such as
name%252Eobj.first=John:
decode_dot_in_keys=True.
- Duplicate keys:
duplicates=qs.Duplicates.COMBINE keeps all values as a
list; use qs.Duplicates.FIRST or qs.Duplicates.LAST to collapse.
- Bracket lists: enabled by default; set
parse_lists=False to treat list
syntax as dictionary keys.
- List limits: default
list_limit is 20; numeric indices at or above the
limit become dictionary keys. The limit also applies cumulatively to lists
grown by duplicate keys, mixed notation, or comma-separated values. Exact-limit
results remain lists; soft overflow becomes a numeric-keyed dictionary, while
raise_on_limit_exceeded=True raises ValueError.
- Comma-separated values such as
a=b,c: comma=True.
- Tokens without
= as None: strict_null_handling=True.
- Custom delimiters:
delimiter=";" or delimiter=re.compile(r"[;,]").
- Legacy charset input:
charset=qs.Charset.LATIN1; use
charset_sentinel=True when a form may include utf8=... to signal the real
charset.
- HTML numeric entities:
interpret_numeric_entities=True, usually with
Latin-1 or charset sentinel handling.
- Untrusted input: keep
depth, parameter_limit, and list_limit bounded;
use strict_depth=True plus raise_on_limit_exceeded=True when callers need
hard failures instead of soft limiting.
Example for a request query:
import qs_codec as qs
params = qs.decode(
"?filter.status=open&tag=python&tag=web",
qs.DecodeOptions(
ignore_query_prefix=True,
allow_dots=True,
duplicates=qs.Duplicates.COMBINE,
),
)
assert params == {"filter": {"status": "open"}, "tag": ["python", "web"]}
Encode Recipes
Use these options with qs.encode(data, qs.EncodeOptions(...)):
- List style defaults to
qs.ListFormat.INDICES:
tags%5B0%5D=python&tags%5B1%5D=web.
- Empty brackets:
list_format=qs.ListFormat.BRACKETS.
- Repeated keys:
list_format=qs.ListFormat.REPEAT.
- Comma-separated values:
list_format=qs.ListFormat.COMMA.
- Single-item comma lists that must round-trip as lists:
comma_round_trip=True.
- Drop
None items before comma-joining lists: comma_compact_nulls=True.
- Dot notation for nested dictionaries:
allow_dots=True.
- Literal dots in keys:
encode_dot_in_keys=True; leave allow_dots
unspecified or set it explicitly based on whether nested paths should use
dot notation.
- Add a leading
?: add_query_prefix=True.
- Custom pair delimiter:
delimiter=";".
- Preserve readable bracket/dot keys while encoding values:
encode_values_only=True.
- Disable percent encoding entirely for debugging or documented examples:
encode=False.
- Emit
None without =: strict_null_handling=True.
- Omit
None keys: skip_nulls=True.
- Emit empty lists as
foo[]: allow_empty_lists=True.
- Omit arbitrary keys by filtering them out of the input mapping before
encoding; avoid internal sentinels in application snippets.
- Legacy form spaces as
+: format=qs.Format.RFC1738; the default is
qs.Format.RFC3986, which emits spaces as %20.
- Legacy charset output:
charset=qs.Charset.LATIN1; use
charset_sentinel=True to prepend the utf8=... sentinel.
- Custom behavior: use
encoder, serialize_date, sort, or filter when
the target API needs special scalar encoding, date formatting, stable key
order, or selected fields.
- Maximum traversal depth:
max_depth=<positive int>; None means unbounded by
this option.
Example for an API that expects repeated keys:
import qs_codec as qs
query = qs.encode(
{
"q": "query strings",
"tag": ["python", "web"],
},
qs.EncodeOptions(
list_format=qs.ListFormat.REPEAT,
add_query_prefix=True,
),
)
assert query == "?q=query%20strings&tag=python&tag=web"
Combinations To Check
Warn or adjust before giving code for these cases:
qs.DecodeOptions(decode_dot_in_keys=True, allow_dots=False) is invalid.
parameter_limit must be positive or float("inf"); use
raise_on_limit_exceeded=True to raise when the limit is exceeded instead of
silently truncating.
list_limit has nuanced list-construction behavior; negative values disable
numeric-index list parsing, and raise_on_limit_exceeded=True turns list
limit violations into ValueError. With comma=True, a flat comma value is
checked before value decoding, while a comma group assigned through []=
counts as one outer list element.
- Built-in charset handling supports only
qs.Charset.UTF8 and
qs.Charset.LATIN1; other encodings require a custom encoder or decoder.
EncodeOptions.encoder is ignored when encode=False.
- Combining
encode_values_only=True and encode_dot_in_keys=True encodes only
dots in keys; values remain otherwise unchanged.
DecodeOptions.comma parses simple comma-separated values, but does not
decode nested dictionary syntax such as a={b:1},{c:d}.
encode(None), scalar roots, empty dictionaries, and empty containers
generally produce an empty string.
- The standard library and many web frameworks flatten duplicates or nested
query syntax. Prefer
qs.decode on the raw query string when qs-style nested
or repeated values matter.
Response Shape
For code-generation requests, answer with:
- A short statement of assumptions, especially list format, null handling,
charset, prefix handling, and whether input is trusted.
- One concrete Python snippet using
qs.decode, qs.encode, qs.loads, or
qs.dumps.
- A brief explanation of only the options used.
- A small verification example, such as an expected dictionary, expected query
string, or a
pytest assertion.
Keep snippets application-oriented. Prefer public API imports from qs_codec;
do not ask users to import from qs_codec.src or private modules.
1---2name: qs-codec3description: Use this skill whenever a user wants to install, configure, troubleshoot, or write Python application code for encoding and decoding nested query strings with the qs-codec package, including comparing or composing qs-codec with urllib.parse.urlencode, parse_qs, parse_qsl, and URL components. This skill helps produce practical qs_codec.decode, qs_codec.encode, qs_codec.loads, and qs_codec.dumps snippets, choose DecodeOptions and EncodeOptions, explain option tradeoffs, and avoid qs-codec edge-case pitfalls around lists, dot notation, duplicates, null handling, charset sentinels, depth limits, URL replacement, and untrusted input.4---56# qs-codec Usage Assistant78Help users parse and build query strings with the Python `qs-codec` package.9Focus on user application code and interoperability outcomes, not repository10maintenance.1112## Start With Inputs1314Before producing a final snippet, collect only the missing details that change15the code:1617- Runtime: Python script, web framework, tests, library code, or generated18 example.19- Direction: decode an incoming query string, encode Python data, or normalize20 query-string handling around an existing URL/request object.21- The actual query string or Python structure when available.22- Target API convention for lists: indexed brackets, empty brackets, repeated23 keys, or comma-separated values.24- Whether the query may include a leading `?`, dot notation, literal dots in25 keys, duplicate keys, custom delimiters, comma-separated lists, `None` flags,26 Latin-1/legacy charset behavior, or untrusted user input.2728Do not over-ask when the desired behavior is obvious. State assumptions in the29answer and give the user a concrete snippet they can paste.3031## Installation3233Install the package from PyPI:3435```bash36python -m pip install qs-codec37```3839Use the package-level public API:4041```python42import qs_codec as qs43```4445When snippets use regex delimiters, dates, or custom codecs, include the needed46standard-library imports such as `re`, `datetime`, `codecs`, or `typing`.4748## Base Patterns4950Decode a query string into nested Python values:5152```python53import qs_codec as qs5455params = qs.decode("a[b][c]=d&tags[]=python&tags[]=web")56assert params == {"a": {"b": {"c": "d"}}, "tags": ["python", "web"]}57```5859Encode nested Python values into a query string:6061```python62import qs_codec as qs6364query = qs.encode({65 "a": {"b": {"c": "d"}},66 "tags": ["python", "web"],67})68assert query == "a%5Bb%5D%5Bc%5D=d&tags%5B0%5D=python&tags%5B1%5D=web"69```7071Use `qs.loads(...)` as a string-only alias for `qs.decode(...)`, and72`qs.dumps(...)` as an alias for `qs.encode(...)`.7374## Standard-Library Codec Selection7576Choose `urllib.parse.urlencode`, `parse_qs`, and `parse_qsl` for conventional77flat `application/x-www-form-urlencoded` data. Choose `qs-codec` when callers78need nested dictionaries or lists, Node `qs` interoperability, configurable79list, duplicate, or null semantics, or matching structure-aware encode/decode80behavior.8182Apply these distinctions when comparing the APIs:8384- Treat `urlencode` as a flat encoder. With `doseq=True`, expand sequence85 values as repeated keys; use `ListFormat.REPEAT` for the equivalent86 `qs.encode` output. Do not pass nested mappings to `urlencode` expecting87 recursive query paths.88- Account for different defaults: `urlencode` emits spaces as `+` and uses89 Python scalar spellings such as `True` and `None`; `qs.encode` defaults to90 `%20`, lowercase booleans, and an empty value for `None`.91- Treat `parse_qs` as a grouped flat parser whose dictionary values are always92 lists. It leaves bracket syntax in literal keys, drops blanks unless93 `keep_blank_values=True`, and cannot distinguish a name-only token from an94 explicit empty value.95- Prefer `parse_qsl` when flat pair order and interleaved duplicates matter. It96 returns an ordered list of name/value pairs, but otherwise shares97 `parse_qs`'s literal bracket, blank-value, and null-distinction limitations.98 It also percent-decodes names and values and normalizes `+` and `%20`, so do99 not present its output as a lossless representation of the raw query.100- Use `qs.decode` to reconstruct bracket or dot paths. A singleton normally101 remains scalar, duplicate policies support combine/first/last, and102 `strict_null_handling=True` distinguishes a bare name as `None` from an103 explicit empty string.104- Note that all three parsers leave primitive values as strings. `parse_qs` and105 `qs.decode` combine repeated flat keys by default, while `parse_qsl` retains106 each occurrence. For resource limits, compare the standard-library parsers'107 `max_num_fields` with `DecodeOptions.parameter_limit`, `depth`, and108 `list_limit` plus `raise_on_limit_exceeded`.109110Do not preprocess a complete URL's raw query with `parse_qs` or `parse_qsl`111before `qs.decode`; doing so flattens structured syntax and loses name-only112distinctions.113114## Standard-Library URL Recipes115116Use `urllib.parse.urlsplit` to separate URL parsing from qs decoding. Pass the117encoded `.query` component directly to `qs.decode`; do not call `unquote`,118`unquote_plus`, `parse_qs`, or `parse_qsl` first. Pre-decoding can turn escaped119delimiters into structure, double-decode percent signs, and flatten qs bracket120syntax.121122```python123from urllib.parse import urlsplit124125import qs_codec as qs126127parts = urlsplit(128 "https://example.com/search?filter%5Bname%5D=Jane%20Doe&flag#results"129)130params = qs.decode(131 parts.query,132 qs.DecodeOptions(strict_null_handling=True),133)134135assert params == {"filter": {"name": "Jane Doe"}, "flag": None}136```137138For a `bytes` URL, `urlsplit` returns a byte query while `qs.decode` accepts139text. Use `.query.decode("ascii")` only when the application boundary guarantees140a conforming ASCII percent-encoded URL; otherwise ask the caller to define the141outer byte-decoding policy.142143Replace a query with freshly encoded data:144145```python146updated = parts._replace(147 query=qs.encode({148 "filter": {"name": "John Doe"},149 "tags": ["a", "b"],150 }),151).geturl()152```153154Apply these constraints when recommending URL composition:155156- Keep `EncodeOptions.add_query_prefix=False` when assigning to157 `SplitResult.query`; a prefixed encoded value creates `??`.158- Default percent-encoded output is appropriate for replacement. Treat159 `encode=False`, `encode_values_only=True`, custom encoders, and raw query text160 as caller-managed because they can emit `#`, `&`, `?`, or malformed percent161 escapes.162- Describe `_replace(query=...).geturl()` as replacement, not append or merge.163 It intentionally discards the existing query and may normalize URL spelling;164 empty encoded output removes an explicit query delimiter.165- Do not propose a generic append helper when existing and new queries may use166 different delimiters. Mixing `&` and `;` cannot be interpreted generally167 without choosing a parser and rewriting one side.168- Do not decode and re-encode an arbitrary existing query to "normalize" it.169 That can regroup interleaved duplicates, convert bare names to empty values,170 change list formats and delimiters, reorder tokens, and select new percent171 spellings.172- Use the direct standard-library expression for raw replacement. A wrapper173 around `_replace(query=raw).geturl()` does not add validation or escaping.174175## Decode Recipes176177Use these options with `qs.decode(query, qs.DecodeOptions(...))`:178179- Leading question mark: `ignore_query_prefix=True`.180- Dot notation such as `a.b=c`: `allow_dots=True`.181- Double-encoded literal dots in keys such as `name%252Eobj.first=John`:182 `decode_dot_in_keys=True`.183- Duplicate keys: `duplicates=qs.Duplicates.COMBINE` keeps all values as a184 list; use `qs.Duplicates.FIRST` or `qs.Duplicates.LAST` to collapse.185- Bracket lists: enabled by default; set `parse_lists=False` to treat list186 syntax as dictionary keys.187- List limits: default `list_limit` is `20`; numeric indices at or above the188 limit become dictionary keys. The limit also applies cumulatively to lists189 grown by duplicate keys, mixed notation, or comma-separated values. Exact-limit190 results remain lists; soft overflow becomes a numeric-keyed dictionary, while191 `raise_on_limit_exceeded=True` raises `ValueError`.192- Comma-separated values such as `a=b,c`: `comma=True`.193- Tokens without `=` as `None`: `strict_null_handling=True`.194- Custom delimiters: `delimiter=";"` or `delimiter=re.compile(r"[;,]")`.195- Legacy charset input: `charset=qs.Charset.LATIN1`; use196 `charset_sentinel=True` when a form may include `utf8=...` to signal the real197 charset.198- HTML numeric entities: `interpret_numeric_entities=True`, usually with199 Latin-1 or charset sentinel handling.200- Untrusted input: keep `depth`, `parameter_limit`, and `list_limit` bounded;201 use `strict_depth=True` plus `raise_on_limit_exceeded=True` when callers need202 hard failures instead of soft limiting.203204Example for a request query:205206```python207import qs_codec as qs208209params = qs.decode(210 "?filter.status=open&tag=python&tag=web",211 qs.DecodeOptions(212 ignore_query_prefix=True,213 allow_dots=True,214 duplicates=qs.Duplicates.COMBINE,215 ),216)217assert params == {"filter": {"status": "open"}, "tag": ["python", "web"]}218```219220## Encode Recipes221222Use these options with `qs.encode(data, qs.EncodeOptions(...))`:223224- List style defaults to `qs.ListFormat.INDICES`:225 `tags%5B0%5D=python&tags%5B1%5D=web`.226- Empty brackets: `list_format=qs.ListFormat.BRACKETS`.227- Repeated keys: `list_format=qs.ListFormat.REPEAT`.228- Comma-separated values: `list_format=qs.ListFormat.COMMA`.229- Single-item comma lists that must round-trip as lists:230 `comma_round_trip=True`.231- Drop `None` items before comma-joining lists: `comma_compact_nulls=True`.232- Dot notation for nested dictionaries: `allow_dots=True`.233- Literal dots in keys: `encode_dot_in_keys=True`; leave `allow_dots`234 unspecified or set it explicitly based on whether nested paths should use235 dot notation.236- Add a leading `?`: `add_query_prefix=True`.237- Custom pair delimiter: `delimiter=";"`.238- Preserve readable bracket/dot keys while encoding values:239 `encode_values_only=True`.240- Disable percent encoding entirely for debugging or documented examples:241 `encode=False`.242- Emit `None` without `=`: `strict_null_handling=True`.243- Omit `None` keys: `skip_nulls=True`.244- Emit empty lists as `foo[]`: `allow_empty_lists=True`.245- Omit arbitrary keys by filtering them out of the input mapping before246 encoding; avoid internal sentinels in application snippets.247- Legacy form spaces as `+`: `format=qs.Format.RFC1738`; the default is248 `qs.Format.RFC3986`, which emits spaces as `%20`.249- Legacy charset output: `charset=qs.Charset.LATIN1`; use250 `charset_sentinel=True` to prepend the `utf8=...` sentinel.251- Custom behavior: use `encoder`, `serialize_date`, `sort`, or `filter` when252 the target API needs special scalar encoding, date formatting, stable key253 order, or selected fields.254- Maximum traversal depth: `max_depth=<positive int>`; `None` means unbounded by255 this option.256257Example for an API that expects repeated keys:258259```python260import qs_codec as qs261262query = qs.encode(263 {264 "q": "query strings",265 "tag": ["python", "web"],266 },267 qs.EncodeOptions(268 list_format=qs.ListFormat.REPEAT,269 add_query_prefix=True,270 ),271)272assert query == "?q=query%20strings&tag=python&tag=web"273```274275## Combinations To Check276277Warn or adjust before giving code for these cases:278279- `qs.DecodeOptions(decode_dot_in_keys=True, allow_dots=False)` is invalid.280- `parameter_limit` must be positive or `float("inf")`; use281 `raise_on_limit_exceeded=True` to raise when the limit is exceeded instead of282 silently truncating.283- `list_limit` has nuanced list-construction behavior; negative values disable284 numeric-index list parsing, and `raise_on_limit_exceeded=True` turns list285 limit violations into `ValueError`. With `comma=True`, a flat comma value is286 checked before value decoding, while a comma group assigned through `[]=`287 counts as one outer list element.288- Built-in charset handling supports only `qs.Charset.UTF8` and289 `qs.Charset.LATIN1`; other encodings require a custom `encoder` or `decoder`.290- `EncodeOptions.encoder` is ignored when `encode=False`.291- Combining `encode_values_only=True` and `encode_dot_in_keys=True` encodes only292 dots in keys; values remain otherwise unchanged.293- `DecodeOptions.comma` parses simple comma-separated values, but does not294 decode nested dictionary syntax such as `a={b:1},{c:d}`.295- `encode(None)`, scalar roots, empty dictionaries, and empty containers296 generally produce an empty string.297- The standard library and many web frameworks flatten duplicates or nested298 query syntax. Prefer `qs.decode` on the raw query string when qs-style nested299 or repeated values matter.300301## Response Shape302303For code-generation requests, answer with:3043051. A short statement of assumptions, especially list format, null handling,306 charset, prefix handling, and whether input is trusted.3072. One concrete Python snippet using `qs.decode`, `qs.encode`, `qs.loads`, or308 `qs.dumps`.3093. A brief explanation of only the options used.3104. A small verification example, such as an expected dictionary, expected query311 string, or a `pytest` assertion.312313Keep snippets application-oriented. Prefer public API imports from `qs_codec`;314do not ask users to import from `qs_codec.src` or private modules.